2
\$\begingroup\$

I have started a hobby project and would like to integrate better and more thorough testing practices. Since I am not too used to it, I'd love to have some feedback on the sample code below. In particular, I'd be interested in the best practices surrounding setting up data necessary to feed a particular testing function or set of functions. As it probably matters, I intend to usepytest.

Here is the sample code from the first testing module in my repo so far:

from accounts import Account from accounts import AccountRollupfrom accounts import AccountTypefrom accounts import AmountTypefrom accounts import JournalEntryfrom accounts import JournalEntryLeg# Setup chart of accountscash_account = Account(1, "Cash", AccountType.Asset)mortgage_account = Account(2, "Mortgage", AccountType.Liability)revenue_account = Account(3, "Revenues", AccountType.Income)balance_sheet = AccountRollup([cash_account, mortgage_account])income_statement = AccountRollup(revenue_account)chart_of_accounts = AccountRollup([balance_sheet, income_statement])def test_journal_entry_balance():    # Journal entry is balanced if Debit == Credit    jnl = JournalEntry(        [            JournalEntryLeg(cash_account, 50, AmountType.Debit),            JournalEntryLeg(cash_account, 50, AmountType.Debit),            JournalEntryLeg(mortgage_account, 100, AmountType.Credit),        ]    )    assert jnl.is_balanced()    # Journal entry is not balanced if Debit != Credit    jnl = JournalEntry(        [            JournalEntryLeg(cash_account, 100, AmountType.Debit),            JournalEntryLeg(mortgage_account, 50, AmountType.Credit),        ]    )    assert not jnl.is_balanced()    # Journal entry is not balanced even if posting negative amounts    jnl = JournalEntry(        [            JournalEntryLeg(cash_account, 100, AmountType.Debit),            JournalEntryLeg(mortgage_account, -100, AmountType.Debit),        ]    )    assert not jnl.is_balanced()    # Test floating-point precision (This test should fail, just testing pytest)    jnl = JournalEntry(        [            JournalEntryLeg(cash_account, 100, AmountType.Debit),            JournalEntryLeg(mortgage_account, 99.9, AmountType.Credit),        ]    )        assert jnl.is_balanced()

If I had to guess areas of improvement:

  • I find the code very repetitive and verbose. It could probably be worthwhile grouping variable assignments and assertions together instead of separated by test case.
  • Set variable assignments in global variables doesn't look very too clean.
askedMar 11, 2021 at 0:24
ApplePie's user avatar
\$\endgroup\$

2 Answers2

2
\$\begingroup\$

The first thing I would point out is that currently, you have multiple test cases (or multiple asserts) inside a single test function. A major downside of this is that if any of the asserts fail, the test function will exit immediately and not run the remaining test cases, making locating and debugging failing tests all that bit harder.

There are a couple of approaches to fix this:

  1. Have a separate test function for each, with the name of the function describing precisely what the test does. I typically follow the format oftest_<subject>_with_<something>_check_<condition>. For example, you could split your current code into 4 separate test functions named:

    • test_balance_with_debit_equal_to_credit_check_balanced
    • test_balance_with_debit_greater_than_credit_check_not_balanced
    • test_balance_with_negative_balance_amounts_check_not_balanced
    • test_balance_with_small_float_difference_check_not_balanced
  2. Use pytest'sparameterize functionality to create sub-tests for us. I personally wouldn't use this approach for the code you have provided, I favour the more explicit result of #1 until we had many more test cases to consider.

As for the global variables, for the size of this, I don't think it's too bad but you might find pytest'sfixtures concept useful in the future. These are useful for employing an Arrange-Act-Assert (AAA) pattern of testing[1][2] and allow you to extract common data or setup to be reused easily.

@pytest.fixturedef cash_account():   return Account(1, "Cash", AccountType.Asset)@pytest.fixturedef mortgage_account():   return Account(2, "Mortgage", AccountType.Asset)@pytest.fixturedef revenue_account():   return Account(3, "Revenues", AccountType.Asset)# name the fixtures you would like to use as parameters to the testdef test_balance_with_debit_equal_to_credit_check_balanced(cash_account, mortgage_account):    journal = JournalEntry(        [            JournalEntryLeg(cash_account, 50, AmountType.Debit),            JournalEntryLeg(cash_account, 50, AmountType.Debit),            JournalEntryLeg(mortgage_account, 100, AmountType.Credit),        ]    )    assert jnl.is_balanced()

You can also build fixtures from other fixtures like so:

@pytest.fixturedef account_charter(cash_account, mortgage_account, revenue_account):   balance_sheet = AccountRollup([cash_account, mortgage_account])   income_statement = AccountRollup(revenue_account)   return AccountRollup([balance_sheet, income_statement])

Thechart_of_accounts variable is unused in the example you provided so I'm not sure if this particular example is relevant but hopefully, you will find a use for the technique elsewhere.

Useful Reading

Pytest covers a bit about AAA in its fixtures documentation I linked above but I also very much liked these blog posts on the topic:

[1] -AAA Testing Pattern: Part 1

[2] -AAA Testing Pattern: Part 2

answeredMar 18, 2021 at 21:35
James Neill's user avatar
\$\endgroup\$
1
\$\begingroup\$

Definitely not the most important issue, but at least for my brain, this is acase where some short convenience variables -- with contextual help and anarrow scope -- can really help code readability scannability. Justhas a nicer vibe too.

def get_journal_entry(*tups):    return JournalEntry([JournalEntryLeg(*t) for t in tups])def test_journal_entry_balance():    ca = cash_account    mo = mortgage_account    d = AmountType.Debit    c = AmountType.Credit    # Journal entry is balanced if Debit == Credit    jnl = get_journal_entry(        (ca, 50, d),        (ca, 50, d),        (mo, 100, c),    )    assert jnl.is_balanced()    # Journal entry is not balanced if Debit != Credit    jnl = JournalEntry(        (ca, 100, d),        (mo, 50, d),    )    assert not jnl.is_balanced()    # Journal entry is not balanced even if posting negative amounts    jnl = JournalEntry(        (ca, 100, d),        (mo, -100, d),    )    assert not jnl.is_balanced()    # Test floating-point precision (This test should fail, just testing pytest)    jnl = JournalEntry(        (ca, 100, d),        (mo, 99.9, c),    )    assert jnl.is_balanced()
answeredMar 11, 2021 at 3:04
FMc's user avatar
\$\endgroup\$

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.