Package: pytest-describe
Describe-style plugin for pytestpytest-describeis a plugin forpytestthat allows tests to be written in arbitrary nested describe-blocks,
similar to RSpec (Ruby) and Jasmine (JavaScript).The main inspiration for this was
avideoby Gary Bernhardt.InstallationYou guessed it:pipinstallpytest-describeUsagePytest will automatically find the plugin and use it when you run pytest.
Running pytest will show that the plugin is loaded:$pytest...
plugins:describe-2.2.0
...Tests can now be written in describe-blocks.
Here is an example for testing a Wallet class:importpytestclassWallet:def__init__(self,initial_amount=0):self.balance=initial_amountdefspend_cash(self,amount):ifself.balance<amount:raiseValueError(f'Not enough available to spend{amount}')self.balance-=amountdefadd_cash(self,amount):self.balance+=amountdefdescribe_wallet():defdescribe_start_empty():@pytest.fixturedefwallet():returnWallet()definitial_amount(wallet):assertwallet.balance==0defadd_cash(wallet):wallet.add_cash(80)assertwallet.balance==80defspend_cash(wallet):withpytest.raises(ValueError):wallet.spend_cash(10)defdescribe_with_starting_balance():@pytest.fixturedefwallet():returnWallet(20)definitial_amount(wallet):assertwallet.balance==20defdescribe_adding():defadd_little_cash(wallet):wallet.add_cash(5)assertwallet.balance==25defadd_much_cash(wallet):wallet.add_cash(980)assertwallet.balance==1000defdescribe_spending():defspend_cash(wallet):wallet.spend_cash(15)assertwallet.balance==5defspend_too_much_cash(wallet):withpytest.raises(ValueError):wallet.spend_cash(25)The default prefix for describe-blocks isdescribe_, but you can configure it
in the pytest/python configuration file viadescribe_prefixesor
via the command line option--describe-prefixes.For example in yourpyproject.toml:[tool.pytest.ini_options]describe_prefixes=["custom_prefix_"]Functions prefixed with_in the describe-block are not collected as tests.
This can be used to group helper functions. Otherwise, functions inside the
describe-blocks need not follow any special naming convention.defdescribe_function():def_helper():return"something"defit_does_something():value=_helper()...Why bother?I've found that quite often my tests have one "dimension" more than my production
code. The production code is organized into packages, modules, classes
(sometimes), and functions. I like to organize my tests in the same way, but
tests also have differentcasesfor each function. This tends to end up with
a set of tests for each module (or class), where each test has to name both a
function and acase. For instance:deftest_my_function_with_default_arguments():deftest_my_function_with_some_other_arguments():deftest_my_function_throws_exception():deftest_my_function_handles_exception():deftest_some_other_function_returns_true():deftest_some_other_function_returns_false():It's much nicer to do this:defdescribe_my_function():defwith_default_arguments():defwith_some_other_arguments():defit_throws_exception():defit_handles_exception():defdescribe_some_other_function():defit_returns_true():defit_returns_false():It has the additional advantage that you can have marks and fixtures that apply
locally to each group of test function.With pytest, it's possible to organize tests in a similar way with classes.
However, I think classes are awkward. I don't think the convention of using
camel-case names for classes fit very well when testing functions in different
cases. In addition, every test function must take a "self" argument that is
never used.The pytest-describe plugin allows organizing your tests in the nicer way shown
above using describe-blocks.Shared BehaviorsIf you've used rspec's shared examples or test class inheritance, then you may
be familiar with the benefit of having the same tests apply to
multiple "subjects" or "suts" (system under test).frompytestimportfixturefrompytest_describeimportbehaves_likedefa_duck():defit_quacks(sound):assertsound=="quack"@behaves_like(a_duck)defdescribe_something_that_quacks():@fixturedefsound():return"quack"# the it_quacks test in this describe will pass@behaves_like(a_duck)defdescribe_something_that_barks():@fixturedefsound():return"bark"# the it_quacks test in this describe will fail (as expected)Fixtures defined in the block that includes the shared behavior take precedence
over fixtures defined in the shared behavior. This rule only applies to
fixtures, not to other functions (nested describe blocks and tests). Instead,
they are all collected as separate tests.