Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert unittest class base tests to pytest function tests #553

Merged
merged 15 commits into from
Oct 25, 2021

Conversation

rootart
Copy link
Contributor

@rootart rootart commented Oct 9, 2021

Many thanks for the great package folks 🤗!
I managed to convert the majority of tests from unittest class base tests to pytest (related issue #521 )and will appreciate your feedback and reviews.
SessionTests tests were not migrated as they look to be very much coupled with ones from Django.

Fixes #521

@codecov
Copy link

codecov bot commented Oct 9, 2021

Codecov Report

Merging #553 (24f6c8a) into master (6ff7c54) will decrease coverage by 26.0%.
The diff coverage is 14.7%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #553      +/-   ##
=========================================
- Coverage    83.8%   57.8%   -25.9%     
=========================================
  Files          21      39      +18     
  Lines        1175    2464    +1289     
  Branches       60      62       +2     
=========================================
+ Hits          984    1424     +440     
- Misses        185    1032     +847     
- Partials        6       8       +2     
Flag Coverage Δ
mypy 34.0% <14.7%> (+0.3%) ⬆️
tests 83.1% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
tests/test_session.py 6.8% <6.8%> (ø)
tests/test_backend.py 14.2% <8.7%> (ø)
tests/test_client.py 30.1% <30.1%> (ø)
tests/test_cache_options.py 33.8% <33.8%> (ø)
tests/test_connection_string.py 66.7% <66.7%> (ø)
tests/conftest.py 75.0% <75.0%> (ø)
tests/settings/sqlite.py 100.0% <100.0%> (ø)
tests/settings/sqlite_lz4.py 100.0% <0.0%> (ø)
tests/test_hashring.py 19.1% <0.0%> (ø)
tests/settings/sqlite_sentinel.py 100.0% <0.0%> (ø)
... and 10 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6ff7c54...24f6c8a. Read the comment docs.

Copy link
Member

@WisdomPill WisdomPill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job and thank you for contributing. We were talking in the past too separate classes in different files, could you separate tests that were in the same class in different files?

I did not review everything because it seems that the same suggestions will be made for the rest of the file and I will need anyway to review everything after the splitting and small fixes will be made

res = self.cache.set("test_key_nx", 1, nx=True)
self.assertTrue(res)
res = default_cache.set("test_key_nx", 1, nx=True)
assert res
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer it to be more specific

Suggested change
assert res
assert res == True



@pytest.fixture()
def available_caches(settings):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add a little bit of type hints? and maybe also separate them? just so that it looks cleaner and you do not have to do the unpacking in every test

res = self.cache.delete_pattern("*foo-a*")
self.assertTrue(bool(res))
res = cache.delete_pattern("*foo-a*")
assert bool(res)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to check if it is True

Suggested change
assert bool(res)
assert res is True

self.assertFalse(res)
default_cache.set("test_key_nx", 1)
res = default_cache.set("test_key_nx", 2, timeout=2, nx=True)
assert not res
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert not res
assert res is False

res = self.cache.delete("a")
self.assertFalse(bool(res))
res = default_cache.delete("a")
assert not bool(res)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert not bool(res)
assert res is False

assert res
res = default_cache.delete("b")
assert type(res) == bool
assert not res
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert not res
assert res is False

mocker.patch("django_redis.cache.DJANGO_VERSION", new=(3, 1, 0, "final", 0))
default_cache.set("a", 1)
res = default_cache.delete("a")
assert type(res) == bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's keep isinstance for simplicity and consistency

Suggested change
assert type(res) == bool
assert isinstance(res, bool)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I've been using https://github.com/pytest-dev/unittest2pytest to convert larger test classes and didn't thoroughly check results.

assert type(res) == bool
assert res
res = default_cache.delete("b")
assert type(res) == bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's keep isinstance for simplicity and consistency

Suggested change
assert type(res) == bool
assert isinstance(res, bool)

def test_delete_many(self, default_cache):
default_cache.set_many({"a": 1, "b": 2, "c": 3})
res = default_cache.delete_many(["a", "b"])
assert bool(res)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert bool(res)
assert res is True

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deletion of the multiple keys returns back the number of deleted objects and therefore I used assert bool(res) is True to cover these cases.

assert res == {"c": 3}

res = default_cache.delete_many(["a", "b"])
assert not bool(res)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert not bool(res)
assert res is False

@rootart
Copy link
Contributor Author

rootart commented Oct 9, 2021

Hi @WisdomPill and thank you for the review.

Great job and thank you for contributing. We were talking in the past too separate classes in different files, could you separate tests that were in the same class in different files?

Sure, I will work on it.

I did not review everything because it seems that the same suggestions will be made for the rest of the file and I will need anyway to review everything after the splitting and small fixes will be made

That totally makes sense and I also noticed that a lot of issues were related to the automatic unittest -> pytest migration tool that I used and I wasn't very careful reviewing results 😅

@WisdomPill
Copy link
Member

It will take me some time to review the whole PR because during the week I do not have much time.
Meanwhile I see that codecov is looking for coverage in tests/ directory, which is not good so maybe it should be filtered out.

Last but not least, I want to raise a question, is there a reason to test for type after all?
Because if you have for example "1" and 1 you can just check if they are equal, it will throw an error if the type is different.
So I think that probably to check the type with isinstance is just redundant.

Python 3.9.1 (default, Jan  8 2021, 17:17:43) 
[Clang 12.0.0 (clang-1200.0.32.28)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> "1" == 1
False
>>> 

@rootart rootart force-pushed the pytest-migration branch 2 times, most recently from 4ca48a4 to 151b8e1 Compare October 11, 2021 20:46
@rootart
Copy link
Contributor Author

rootart commented Oct 11, 2021

I was probably a bit confused regarding your suggestion of adding type annotations to the provided fixtures and added them to all tests :) Updated version removes that checks and I assume should fix issues with codecov.

Last but not least, I want to raise a question, is there a reason to test for type after all?
No, I don't see many benefits in testing for types and as you fairly pointed most of it will be correctly resolved on the assertion level by the pytest.

@WisdomPill WisdomPill changed the title #521, Convert unittest class base tests to pytest function tests Convert unittest class base tests to pytest function tests Oct 19, 2021
@rootart
Copy link
Contributor Author

rootart commented Oct 21, 2021

@WisdomPill would be great if you can approve workflow execution to check tests 🙇

@terencehonles
Copy link
Contributor

@WisdomPill would be great if you can approve workflow execution to check tests bow

Done

Copy link
Contributor

@terencehonles terencehonles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks good to me, but I guess there's a little bit of a difference of opinion on the asserts 😅

def make_key(key, prefix, version):
return f"{prefix}#{version}#{key}"
@pytest.fixture
def cache() -> Generator[RedisCache, None, None]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're not supplying the second two type variables this is a shorthand for the same thing

Suggested change
def cache() -> Generator[RedisCache, None, None]:
def cache() -> Iterable[RedisCache]:

res = self.cache.set("test_key_nx", 1, nx=True)
self.assertTrue(res)
res = cache.set("test_key_nx", 1, nx=True)
assert bool(res) is True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WisdomPill do you strongly prefer assert bool(res) is True over assert res and assert res is False over assert not res? I find it a bit wordy since it means the same thing, but I understand you already requested the change in #553 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer assert res is True because it more readable and correct.

From python zen

Python 3.9.1 (default, Jan  8 2021, 17:17:43) 
[Clang 12.0.0 (clang-1200.0.32.28)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
>>> 

more specifically the two points,

  • Explicit is better than implicit
  • Readability counts.

It is more correct and more elegant (imho), because it also checks the type, like the following

>>> True is True
True
>>> bool([1])
True
>>> bool("hello")
True
>>> bool(1)
True
>>> 1 is True
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
False
>>> [1] is True
False
>>> "hello" is True
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
False
>>> 

This is why I am used to not leave any doubt about why there should be

if len(li):
    pass

instead of

if li:
    pass

Copy link
Contributor

@terencehonles terencehonles Oct 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I agree about it being more readable. Both are quite readable. It's definitely more pedantic, and I know it enforces the type so it is more accurate. The question would be does the accuracy matter.

It sounds like the simple answer would have just been "yes", and we can just leave it as is (I know it might also be frustrating for @rootart to be asked to switch it back so they may just want to leave it as is too 😅 )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no problem. let's get this through, if you have reviewed it it's okay for me, in case I will change the tests to be more pedantic when I will have time for it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I might have made some mistakes, by saying that assert bool(res) is True would be the right way.
I disagree, I al in favor of assert res is True


@patch("django_redis.cache.DJANGO_VERSION", (3, 1, 0, "final", 0))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be OK to leave @patch as a decorator (here and the rest of the PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I will still keep mocker for patching objects and properties if possible.



@pytest.fixture
def key_prefix_cache(settings: SettingsWrapper) -> Generator[RedisCache, None, None]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def key_prefix_cache(settings: SettingsWrapper) -> Generator[RedisCache, None, None]:
def key_prefix_cache(settings: SettingsWrapper) -> Iterable[RedisCache]:

Comment on lines +111 to +114
caches_setting = copy.deepcopy(settings.CACHES)
caches_setting["default"]["KEY_FUNCTION"] = "test_cache_options.make_key"
caches_setting["default"]["REVERSE_KEY_FUNCTION"] = "test_cache_options.reverse_key"
settings.CACHES = caches_setting
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe you need the copy 🤔 or is this because the settings wrapper doesn't understand mutations to the underlying dict?

Suggested change
caches_setting = copy.deepcopy(settings.CACHES)
caches_setting["default"]["KEY_FUNCTION"] = "test_cache_options.make_key"
caches_setting["default"]["REVERSE_KEY_FUNCTION"] = "test_cache_options.reverse_key"
settings.CACHES = caches_setting
settings.CACHES["default"]["KEY_FUNCTION"] = "test_cache_options.make_key"
settings.CACHES["default"]["REVERSE_KEY_FUNCTION"] = "test_cache_options.reverse_key"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like that direct mutation doesn't trigger the required signals to reload the configuration.

def test_close_disconnect_settings_cache(
self, cache_client: DefaultClient, mocker: MockerFixture
):
settings.CACHES[DEFAULT_CACHE_ALIAS]["OPTIONS"]["CLOSE_CONNECTION"] = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about the last comment, does settings need to be a fixture so that this doesn't affect the global state for this config?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, will replace it with a fixture to avoid changing the global state.

@terencehonles terencehonles merged commit 5bf67ab into jazzband:master Oct 25, 2021
@terencehonles
Copy link
Contributor

I reviewed everything and made some minor changes, but it looks good @rootart! Thanks for all your efforts 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Convert unittest class base tests to pytest function tests
4 participants