{"id":22196711,"url":"https://github.com/druids/germanium","last_synced_at":"2025-09-01T03:39:34.587Z","repository":{"id":13783914,"uuid":"16479134","full_name":"druids/germanium","owner":"druids","description":"Helpful methods for Python Selenium","archived":false,"fork":false,"pushed_at":"2024-06-16T14:13:46.000Z","size":172,"stargazers_count":5,"open_issues_count":0,"forks_count":10,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-27T03:07:07.511Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/druids.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.txt","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-02-03T12:43:12.000Z","updated_at":"2024-06-16T14:13:35.000Z","dependencies_parsed_at":"2024-03-15T11:43:50.890Z","dependency_job_id":"bca09f25-8e7d-4681-ab9a-e51e72887724","html_url":"https://github.com/druids/germanium","commit_stats":null,"previous_names":[],"tags_count":74,"template":false,"template_full_name":null,"purl":"pkg:github/druids/germanium","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Fgermanium","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Fgermanium/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Fgermanium/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Fgermanium/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/druids","download_url":"https://codeload.github.com/druids/germanium/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Fgermanium/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273070287,"owners_count":25040248,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-01T02:00:09.058Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-02T14:16:13.402Z","updated_at":"2025-09-01T03:39:34.563Z","avatar_url":"https://github.com/druids.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Germanium\n\n[![License](https://img.shields.io/badge/MIT-Clause-blue.svg)](https://opensource.org/licenses/MIT)\n\nThe main reason of the library was to create wrapper over selenium framework for Django unit tests, but over time the library was changed to test utils for the standard Django unit tests.\n\n## Installation\n\n- Install `django-germanium` with the `pip` command:\n\n```bash\npip install django-germanium\n```\n\nSet the germanium test runner in your settings:\n\n```python\nTEST_RUNNER = 'germanium.django.runner.GermaniumDiscoverRunner'\n```\n\n## Configuration\n\n### Fixtures\n\nIf you want to use fixtures for the all test cases which use `GermaniumTestCase` test case you can define path to the file in your django settings with parameter:\n\n```python\nGERMANIUM_GLOBAL_FIXTURES = {\n    'default': [\n        os.path.join(PROJECT_DIR, 'data/common/test/init_data.json'),\n    ]\n}\n```\nThe key `'default'` is name of the database which is configured in your django `DATABASES` setting.\n\n### Multiple DB testing\n\nDjango rollbacks only primary database in `TestCase` by default. With `GermaniumTestCase` you can turn on the rollbacks on the all databases with setting:\n\n```python\nGERMANIUM_TEST_ALL_DATABASES = True\n```\n\n## Test Cases\n\nThe library provides several test cases and mixins which simplify test definitions. There are in the package `test_cases` which is divided into modules:\n * `germanium.tests_cases.default` - main Germanium test cases\n * `germanium.tests_cases.auth` - contains a test case class which can help with user authentication\n * `germanium.tests_cases.client` - test cases with methods helping with testing endpoints and web pages\n * `germanium.tests_cases.rest` - test cases with methods helping with testing REST API\n * `germanium.tests_cases.models` - depricated tests case helping with model tests\n * `germanium.tests_cases.migrations` - a migration tests case helper\n\n### Default\n\nDefault module contains four classes with this inheritance:\n\n![Test cases inheritance](test-cases.png)\n\n#### `GermaniumSimpleTestCaseMixin` \nUpgrades test cases with test all database helper (setting `GERMANIUM_TEST_ALL_DATABASES`) and methods `set_up_class`, `tear_down_class`, `set_up`, `tear_down` which is the same as original methods `setUpClass`, `tearDownClass`, `setUp`, `tearDown` but in the camel case format.\n\nGermanium defines django signals in the module `germanium.signals`. These signals are automatically triggered in the corresponding method `setUpClass`, `tearDownClass`, `setUp` or `tearDown`:\n\n```python\nset_up_class = Signal()\ntear_down_class = Signal()\nset_up = Signal()\ntear_down = Signal()\n```\n\n#### `GermaniumTestCaseMixin` \n\nThe mixin only adds ability to use `GERMANIUM_FIXTURES` setting.\n\n#### `GermaniumTestCase` and `GermaniumSimpleTestCase`\n\nClasses are only connection of mixins and django test cases. No new functionality are added. `GermaniumTestCase` should be used for transaction tests and `GermaniumSimpleTestCase` for non transaction tests.\n\n### Auth\n\nModule implements only one mixin `AuthTestCaseMixin` and helper proxy object `UserProxy` which may help to write tests with user login:\n\n\n```python\nfrom germanium.test_cases.auth import AuthTestCaseMixin\nfrom germanium.test_cases.default import GermaniumTestCase\nfrom germanium.tools import assert_true\nfrom germanium.decorators import login\n\nclass TestCaseWithLogin(AuthTestCaseMixin, GermaniumTestCase):\n\n    def get_user(self):\n        # create your test user\n        return UserProxy(username='test', password='test', user=User.objects.create(username='test', password='test'))\n        \n    def authorize(self, username, password):\n        # your authorization implementation\n        ...\n\n    def logout(self):\n        # your logout implementation\n        ...\n\n    @login\n    def test_logged_user(self):\n        assert_true(self.logged_user.user.is_authenticated)\n\n```\n\nAs you can see in the example you should implement three methods:\n* `logout` - for example logout user via REST API\n* `authorize` - login user (API or standard django login helper)\n* `get_user` - create user and return it in the `UserProxy` proxy object with its username and password\n\nFinally, you can use `@login` decorator to run test with a logged user.\n\n### Client\n\n`ClientTestCase` and `SimpleClientTestCase` are Germanium tests cases with methods for testing HTTP resources. These test cases inherits `AuthTestCaseMixin` too and implements authorize and logout methods. The methods for testing HTTP requests are:\n \n* `get(url, headers=None)` - sends the GET HTTP requests to `url` with headers `headers` and return response\n* `put(url, data={}, headers={})` - sends the PUT HTTP requests to `url` with data `data` and headers `headers` and return response\n* `post(url, data={}, headers={})` - sends the POST HTTP requests to `url` with data `data` and headers `headers` and return response\n* `head(url, headers=None)` - sends the HEAD HTTP requests to `url` with headers `headers` and return response\n* `options(url, headers=None)` - sends the OPTIONS HTTP requests to `url` with headers `headers` and return response\n* `delete(url, headers={})` - sends the DELETE HTTP requests to `url` with headers `headers` and return response\n\n### Rest\n\nRest module is very similar to client module. Its purpose is to test REST API. There are the same test helpers as in the Client test cases and two new helpers:\n\n* `deserialize(resp, content_type)` - deserialize the response to the base python data types according to content type.\n* `serialize(data, content_type)` - serialize data to the format for API request.\n\nOnly JSON serialized is implemented by default. You can add your serializer and deserializer into test case properties `SERIALIZERS` and `DESERIALIZERS`.\n\n### Migrations\n\nTest case `MigrationTestCase` in migrations module help can help with migrations testing. You have to only define properties `migrate_from` and `migrate_to`. Migrations between `migrate_from` and `migrate_to` of the app where is the test case stored are tested:\n\n```python\nfrom germanium.test_cases.migrations import MigrationTestCase\n\nclass YourMigrationTestCase(MigrationTestCase):\n\n    migration_from = '0001_migration'\n    migration_to = '0010_migration'\n```\n\n## Tools\n\nGermanium adds tools helping with testing. It is similar to the nose.tools module which provides a number of testing aids that you may find useful, including decorators for restricting test execution time and testing for exceptions, and all of the same assertX methods found in unittest.TestCase (only spelled in PEP 8#function-names fashion, so assert_equal rather than assertEqual).\n\n### Trivials\n\n#### fail\n\nFail defines a bad test branch which process cannot achieve:\n\n```python\n    from germanium.tools import fail\n\n    def test_with_fail(self):\n        if term_should_not_be_true:\n            fail('optional fail message')\n```\n\n#### assert_not_raises\n\nA context manager similar to `assert_raises` but you want to test that an exception should not be raised:\n\n```python\n    from germanium.tools import assert_not_raises\n\n    def test_assert_not_raises(self):\n        with assert_not_raises(SomeException):\n            code_should_not_raise_some_exception()\n```\n\n#### assert_length_equal\n\nAn assert testing if iterable len has expected number of elements.\n\n```python\n    from germanium.tools import assert_length_equal\n\n    def test_assert_length_equal(self):\n        assert_length_equal([1, 2], 2, 'optional fail message')\n```\n\n#### all_eq_obj, not_none_eq_obj\n\nHelper singletons which can be used for comparing two elements (for example dicts) where matching some value is not required:\n\n```python\n    from germanium.tools import all_eq_obj, not_none_eq_obj, assert_equal, assert_not_equal\n\n    def test_matching_signletons(self):\n        assert_equal({'a': 5, 'b': 6}, {'a': 5, 'b': all_eq_obj})\n        assert_equal({'a': 5, 'b': None}, {'a': 5, 'b': all_eq_obj})\n        assert_not_equal({'a': 5}, {'a': 5, 'b': all_eq_obj})\n        assert_not_equal({'a': 8, 'b': None}, {'a': 5, 'b': all_eq_obj})\n        assert_not_equal({'a': 5, 'b': None}, {'a': 5, 'b': not_none_eq_obj})\n        assert_equal({'a': 5, 'b': 6}, {'a': 5, 'b': not_none_eq_obj})\n```\n\n### Http\n\nHttp tools adds asserts for testing requests and responses:\n\n* `assert_http_ok(resp, msg=None)` - test if the response code is 200\n* `assert_http_created(resp, msg=None)` - test if the response code is 201\n* `assert_http_accepted(resp, msg=None)` - test if the response code is 202 or 204\n* `assert_http_multiple_choices(resp, msg=None)` - test if the response code is 300\n* `assert_http_redirect(resp, msg=None)` - test if the response code is 302\n* `assert_http_see_other(resp, msg=None)` - test if the response code is 303\n* `assert_http_not_modified(resp, msg=None)` - test if the response code is 304\n* `assert_http_bad_request(resp, msg=None)` - test if the response code is 400\n* `assert_http_unauthorized(resp, msg=None)` - test if the response code is 401\n* `assert_http_forbidden(resp, msg=None)` - test if the response code is 403\n* `assert_http_not_found(resp, msg=None)` - test if the response code is 404\n* `assert_http_method_not_allowed(resp, msg=None)` - test if the response code is 405\n* `assert_http_conflict(resp, msg=None)` - test if the response code is 409\n* `assert_http_gone(resp, msg=None)` - test if the response code is 410\n* `assert_http_unprocessable_entity(resp, msg=None)` - test if the response code is 422\n* `assert_http_too_many_requests(resp, msg=None)` - test if the response code is 429\n* `assert_http_application_error(resp, msg=None)` - test if the response code is 500\n* `assert_http_not_implemented(resp, msg=None)` - test if the response code is 501\n* `assert_http_bad_gateway(resp, msg=None)` - test if the response code is 502\n* `assert_http_service_unavailable(resp, msg=None)` - test if the response code is 503\n* `assert_http_gateway_timeout(resp, msg=None)` - test if the response code is 504\n\n### Rest\n\nRest asserts to test response:\n\n* `assert_valid_JSON(data, msg=None)` - data is valid JSON string\n* `assert_valid_JSON_response(resp, msg=None)` - the response content is valid JSON\n* `assert_valid_JSON_created_response(resp, msg=None)` - the response content is valid JSON and its status code is 201\n* `assert_keys(data, expected, msg=None)` - the assert ensures that the keys of the `data` match up to the keys of `expected`.\n\n### Models\n\nHelpers for testing django models:\n\n* `get_pks(iterable)` - return list of models pk from iterable\n* `assert_iterable_equal(first, second, msg=None)` - two iterable objects are equal with no matter on its order\n* `assert_qs_exists(qs, msg=None)` - a queryset is not empty\n* `assert_qs_not_exists(qs, msg=None)` - a queryset is empty  \n* `assert_qs_contains(qs, obj, msg=None)` - a queryset contains the object\n* `assert_qs_not_contains(qs, obj, msg=None)` - a queryset does not contains the object\n* `assert_equal_model_fields(instance, refresh_from_db=False, **field_values)` - an instance fields are properly set. Value `refresh_from_db` defines if object should be reloaded from the DB. Usage: `assert_equal_model_fields(user, username='test, email='test@email.cz')`\n* `assert_num_queries(num, func=None, *args, using=DEFAULT_DB_ALIAS, clear_content_type_cache=False)` - context processors for testing right number of DB queries in test:\n\n```python\n    from germanium.tools import assert_num_queries\n\n    def test_assert_num_queries(self):\n        with assert_num_queries(10, clear_content_type_cache=True):\n            function_which_perform_10_queries()\n```\n\nHelper `assert_num_queries` check if function provides exactly 10 DB queries. Parameter `clear_content_type_cache` defines if Django content type cache should be cleaned before testing.\n\n\n### Django\n\nHelpers related to Django framework:\n\n### test_call_command\n\nHelper for testing django commands. Command stdout and stderr are sent to `/dev/null`\n\n```python\n    from germanium.tools import test_call_command\n\n    def test_command(self):\n        test_call_command('some_commmand')\n```\n\n### capture_commit_callbacks\n\nIt is impossible to test Django on commit in the standard Django transaction test cases because a database doesn't commit data to prevent mutual influence of the tests. For this purpose you can use this context processor:\n\n```python\n    from germanium.tools import capture_commit_callbacks\n    from django.db import transaction\n\n    def test_command(self):\n        with capture_commit_callbacks(execute_on_commit=True):\n            transaction.on_commit(do_something)\n        assert_true(do_something.was_called)\n```\n\nIf you are using `django-chamber` pre commit signal can be triggered with the context processor to with parameter `execute_pre_commit=True`.\n\nFor the cases when function defined in on_commit signal (`do_something`) will init another `on_commit` signal, you can use `execute_on_commit_cascade=True` to call signal in cascade.\n\n## Crawler\n\nCrawler is class which can help with automatic testing of your web page. It browses your web pages from index and searching for bad responses. You can use it in your tests or as a simple script:\n\n```python\n    from django.test.client import Client\n    from germanium.crawler import Crawler, LinkExtractor\n    \n    def pre_request(url, referer, headers):\n        \"\"\"request changer\"\"\"\n        return url, headers\n\n    def post_response(url, referer, resp, exception):\n        \"\"\"response checker\"\"\"\n        your_response_check(resp)\n\n\n    Crawler(Client(), ('/',), self.get_exlude_urls(), pre_request, post_response).run()\n```\n\n## Test decorators\n\nThere are several test decorators which you can use to write shorter and more readable tests.\n\n### Login\n\nDecorators `@login` and `@login_all` can be used with `AuthTestCaseMixin` to simplify user login. The decorator `@login` is used in a single test:\n\n```python\nfrom germanium.decorators import login\n\nclass TestCaseWithLogin(AuthTestCaseMixin, GermaniumTestCase):\n\n    @login\n    def test_logged_user(self):\n        assert_true(self.logged_user.user.is_authenticated)\n```\n\n`@login_all` is used for the test case and it is only shortcut which adds `@login` to all tests in the test case.\n\n```python\nfrom germanium.decorators import login_all\n\n@login_all\nclass TestCaseWithLogin(AuthTestCaseMixin, GermaniumTestCase):\n    \n    def test_logged_user(self):\n        assert_true(self.logged_user.user.is_authenticated)\n```\n\n### Data provider and data consumers\n\nData providers and consumers are a test technique which splits test data preparation and test logics. Data consumer can be simply use to runt test with some test data:\n\n\n```python\nfrom germanium.decorators import data_consumer\n\nclass DataConsumerTestCase(GermaniumTestCase):\n    \n    @data_consumer([1, 2, 3], _output_name='number')\n    def test_data_consumer(self, number):\n        assert_true(number in {1, 2, 3})\n```\n\nThe test `test_data_consumer` will be processed three times for number 1, 2 and 3. Number will be send as test input parameter defined in property `_output_name`.\n\nYou can use more data providers for one test:\n\n```python\nfrom germanium.decorators import data_consumer\n\nclass DataConsumerTestCase(GermaniumTestCase):\n    \n    @data_consumer([1, 2], _output_name='number_a')\n    @data_consumer([3, 4], _output_name='number_b')\n    def test_data_consumer(self, number_a, number_b):\n        assert_true(number_a in {1, 2})\n        assert_true(number_b in {3, 4})\n```\n\nIn this case the test will be called four times with inputs (1, 3), (1, 4), (2, 3) and (2, 4). \n\nYou can define more parameters in one data consumer to achieve the same result:\n\n```python\n    @data_consumer([(1, 3), (1, 4), (2, 3), (2, 3)], _output_name=['number_a', 'number_b'])\n    def test_data_consumer(self, number_a, number_b):\n        assert_true(number_a in {1, 2})\n        assert_true(number_b in {3, 4})\n```\n\nInput data can be get from test case property too:\n\n```python\nfrom germanium.decorators import data_consumer\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    test_data = [(1, 3), (1, 4), (2, 3), (2, 3)]\n\n    @data_consumer('test_data', _output_name=['number_a', 'number_b'])\n    def test_data_consumer(self, number_a, number_b):\n        assert_true(number_a in {1, 2})\n        assert_true(number_b in {3, 4})\n```\n\nSometimes you want to create dynamically data in a function:\n\n```python\ndef get_test_data():\n    for i in range(10):\n        yield i\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    @data_consumer(get_test_data, _output_name='number')\n    def test_data_consumer(self, number):\n        assert_true(number_a \u003c 10)\n```\n\nor as a test case method:\n\n```python\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    def get_test_data(self):\n        for i in range(10):\n            yield i\n\n    @data_consumer('get_test_data', _output_name='number')\n    def test_data_consumer(self, number):\n        assert_true(number \u003c 10)\n```\n\nFor more complex data preparation you can use `@data_provider`:\n\n```python\nfrom germanium.decorators import data_provider\n\n@data_provider(name='number')\ndef get_test_data():\n    return list(range(10))\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    @data_consumer(get_test_data)\n    def test_data_consumer(self, number):\n        assert_true(number \u003c 10)\n```\n\nThe parameter `_output_name` should not be defined because the name is defined in the data provider decorator. But you can rename for the test if you want:\n\n```python\n@data_provider(name='number')\ndef get_test_data():\n    return list(range(10))\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    @data_consumer(get_test_data, _output_name='number_b')\n    def test_data_consumer(self, number_b):\n        assert_true(number_b \u003c 10)\n```\n\nYou can return more named data in the data provider. Not all must be used in the test:\n\n```python\nfrom germanium.decorators import NamedTestData\n\n@data_provider\ndef get_test_data():\n    return NamedTestData(a=1, b=2, c=3)\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    @data_consumer(get_test_data)\n    def test_data_consumer(self, a, b):\n        assert_equal(a, 1)\n        assert_equal(b, 2)\n```\n\nIf you rename data provider output name only first value from named data can be used:\n\n```python\n@data_provider\ndef get_test_data():\n    return NamedTestData(a=1, b=2, c=3)\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    @data_consumer(get_test_data, _output_name='d')\n    def test_data_consumer(self, d):\n        assert_equal(d, 1)\n```\n\nThe output data of one data provider can be used in the second data provider:\n\n```python\n@data_provider(name='a')\ndef get_test_data_a():\n    return 1\n\n@data_provider(name='b')\ndef get_test_data_b(a):\n    return a + 1\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    @data_consumer(get_test_data_a)\n    @data_consumer(get_test_data_b)\n    def test_data_consumer(self, a, b):\n        assert_equal(a, 1)\n        assert_equal(b, 2)\n```\n\nYou can define static input for the data provider:\n\n```python\nfrom germanium.decorators import NamedDataSource\n\n@data_provider(name='b')\ndef get_test_data_b(d):\n    return d + 1\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    @data_consumer(get_test_data_b, d=5)\n    def test_data_consumer(self, b):\n        assert_equal(b, 6)\n```\n\nIf you need to pass data to second data provider which input does not have the same name, you can use `NamedDataSource`:\n\n```python\nfrom germanium.decorators import NamedDataSource\n\n@data_provider(name='a')\ndef get_test_data_a():\n    return 1\n\n@data_provider(name='b')\ndef get_test_data_b(d):\n    return d + 1\n\nclass DataConsumerTestCase(GermaniumTestCase)\n\n    @data_consumer(get_test_data_a)\n    @data_consumer(get_test_data_b, d=NamedDataSource('a'))\n    def test_data_consumer(self, a, b):\n        assert_equal(a, 1)\n        assert_equal(b, 2)\n```\n\nIf you are using django tests and data provider generates data in database the rollback is used after test tear down, therefore every test run has the clean data.\n\n## Storage\n\nThe Django storage defines where the files will be stored and how. For test purposes is better not to store file on disk but keep them in the memory to avoid tests influencing. Class `TestInMemoryStorage` can be used for these purposes. You only have to use `GermaniumTestCase` because storage uses Germanium set up and tear down signals. Storage you can define in your django settings (for test purposes):\n\n```python\nDEFAULT_FILE_STORAGE = 'germanium.storage.TestInMemoryStorage'\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdruids%2Fgermanium","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdruids%2Fgermanium","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdruids%2Fgermanium/lists"}