0

This question is about designing unit tests, something I started learning short ago.

I know the principle that if you are testing too much in a single unitary test it is a smell, either of code or design itself.

Scenario

I am computing a tax return form based on tax figures computed over the course of a year. Those figures are stored in the database in different rows on a monthly basis.

In order to compute the figures for the tax returns, some columns have to be copied "in the right place", other have to be summed up across 12 months.

The final form has a set of few flat columns, a Map divided by month with 10 columns for each entity and another Map that is a view on a different aggregation (hence the sum), where each entry has 4 columns.

Mocking the database is a joke, so I have a consistent set of tax figures along with the expected results hardcoded (this is a kind of code duplication I like to implement).

The problem here is that while I am still writing test code I discover myself writing a lot of testing code.

The method that computes the form should be atomic (i.e. set all the columns at once) because it runs in a single transaction creating a new consistent object, but is all about setting dozens of different columns. I have ended up testing more than 30 different properties.

Simple question

  • Given that my unitary method works on columnA..columnZ at the same time
  • Given that failure of one assertion (thanks to Junit's ErrorCollector) causes the test to fail but evaluates and reports other failed assertions all together

From the test design perspective, and in particular to the principle "don't test too much", is it preferrable to...

  1. Write a single test that performs 26 assertions each every column?
  2. Write multiple tests that run the same method with the same data set but each tests the outcome of a very single property of the output?
  3. Other strategy?

Pseudo code

The following is a sample of how the current code is structured. I really have plenties of assertions

@Test
public void testCreateTaxForm(){

    //Create the mocks, e.g.
    MonthlyDao dao = mock(MonthlyDao.class);
    when(dao.list(any)).thenReturn(getTheTestSet());

    TaxFormManager uut = new TaxFormManagerImpl();
    uut.setDao(dao);

    //Test
    TaxForm form = uut.create(....);

    //Verify

    getErrorCollector().checkThat(form,hasProperty("flatColumnA",equalTo(expectedValueForA));
    getErrorCollector().checkThat(form,hasProperty("flatColumnA",equalTo(expectedValueForB));
    -----
    getErrorCollector().checkThat(form,hasProperty("flatColumnA",equalTo(expectedValueForZ));

    //Following is wrapped in a method for my comfort
    for (Month month: Month.values()) {
        getErrorCollector().checkThat(form,hasProperty("monthMap",hasEntry(equalTo(month),hasProperty("flatMonthlyColumnA",equalTo(....)));
         .............
    }

    for (ExemptionType exemption: ExemptionType.values()) {
        getErrorCollector().checkThat(form,hasProperty("exemptions",hasEntry(equalTo(month),hasProperty("flatExemptionColumnA",equalTo(....)));
         .............
    }

}

Pseudo alternative

In the pseudo alternative, I would have to write dozens of different methods (at least 120, each testing a column in a month) trying to reuse as most initialization logic as possible to avoid the test code base to grow too fat.

public void testTaxReturn_flatColumnA
public void testTaxReturn_flatColumnB
public void testTaxReturn_flatColumnC
public void testTaxReturn_january_columnA
public void testTaxReturn_january_columnB
public void testTaxReturn_january_columnC
public void testTaxReturn_retirementFunds_columnA
public void testTaxReturn_retirementFunds_columnB
public void testTaxReturn_retirementFunds_columnC
5
  • Comment: I am still working since this morning on writing the full code for the first version of the testing code Jul 11, 2019 at 15:59
  • It seems as though you're testing too much in one go - break down the logic into isolated components that operate on minimal data, test each piece of the calculation independently and then just have an integration test to test that the whole thing is saved properly.
    – Ant P
    Jul 11, 2019 at 16:04
  • What should the integration test be testing? All the 100+ columns at once? The method code is not all that complex, it leverages a few for-cycles to be kept simply simple. I'm really interested in digging through this. Jul 11, 2019 at 16:20
  • Would you be happy with your testcase if you could write the verification as checkThat(form, equalTo(expectedForm))? Jul 12, 2019 at 8:09
  • Initially I would say yes, BUUUUUUT, it's hard to tell which property is failed on Hamcrest's equalTo matcher. Either I would write my own custom matcher with my own custom description (lots of checks and LOCs) or write a lot of custom checks Jul 12, 2019 at 8:12

1 Answer 1

3

It is common and usually desirable to have a lot of test code. It's not desirable to cram all your tests together. Unit testing appropriately is a skill that takes practice, but one way to help not write too big single tests is to look at the code under test.

If there's a container in the code, I think of the smallest tests I can possibly write that will test if that code behaves properly when empty, has one item, has multiple items, and other code-specific boundaries. That's 3+ tests for that one container.

If there's an if statement in the code, I think of the smallest tests possible that will test the true and false conditions. That's 2 tests per if statement.

If there's a boolean that is anded together from many conditions, I think of the smallest tests possible that will test each condition being false when the others are true. That's one test for the overall boolean and one for each sub-condition.

If there's a boolean that is ored together from many conditions, I think of the smallest tests possible that will test each condition being true when the others are false. That's one test for the overall boolean and one for each sub-condition.

If there's a loop, I think of the smallest tests possible for the loop never being entered, being executed once, being executed several times, and other boundary conditions. That's 3+ tests for each loop.

If an exception is thrown, I think of the smallest tests possible for getting it to throw. That's 1+ tests for each exception.

You get the idea. You don't test every possible combination of input and output. If your column A is processed using the exact same code as column B, only test the behavior fully for column A, then do a minimal spot check to make sure the column B output is pulling from the right input.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.