Testing on the Toilet: Cleanly Create Test Data
utorok, februára 20, 2018
This article was adapted from a Google Testing on the Toilet (TotT) episode. You can download a printer-friendly version of this TotT episode and post it in your office.
By Ben Yu
Helper methods make it easier to create test data. But they can become difficult to read over time as you need more variations of the test data to satisfy constantly evolving requirements from new tests:
Instead, use the test data builder pattern: create a helper method that returns a partially-built object (e.g., a Builder in languages such as Java, or a mutable object) whose state can be overridden in tests. The helper method initializes logically-required fields to reasonable defaults, so each test can specify only fields relevant to the case being tested:
Also note that tests should never rely on default values that are specified by a helper method since that forces readers to read the helper method’s implementation details in order to understand the test.
You can learn more about this topic at http://www.natpryce.com/articles/000714.html
By Ben Yu
Helper methods make it easier to create test data. But they can become difficult to read over time as you need more variations of the test data to satisfy constantly evolving requirements from new tests:
// This helper method starts with just a single parameter:
Company company = newCompany(PUBLIC);
// But soon it acquires more and more parameters.
// Conditionals creep into the newCompany() method body to handle the nulls,
// and the method calls become hard to read due to the long parameter lists:
Company small = newCompany(2, 2, null, PUBLIC);
Company privatelyOwned = newCompany(null, null, null, PRIVATE);
Company bankrupt = newCompany(null, null, PAST_DATE, PUBLIC);
// Or a new method is added each time a test needs a different combination of fields:
Company small = newCompanyWithEmployeesAndBoardMembers(2, 2, PUBLIC);
Company privatelyOwned = newCompanyWithType(PRIVATE);
Company bankrupt = newCompanyWithBankruptcyDate(PAST_DATE, PUBLIC);
|
Instead, use the test data builder pattern: create a helper method that returns a partially-built object (e.g., a Builder in languages such as Java, or a mutable object) whose state can be overridden in tests. The helper method initializes logically-required fields to reasonable defaults, so each test can specify only fields relevant to the case being tested:
Company small = newCompany().setEmployees(2).setBoardMembers(2).build();
Company privatelyOwned = newCompany().setType(PRIVATE).build();
Company bankrupt = newCompany().setBankruptcyDate(PAST_DATE).build();
Company arbitraryCompany = newCompany().build();
// Zero parameters makes this method reusable for different variations of Company.
// It also doesn’t need conditionals to ignore parameters that aren’t set (e.g. null
// values) since a test can simply not set a field if it doesn’t care about it.
private static Company.Builder newCompany() {
return Company.newBuilder().setType(PUBLIC).setEmployees(100); // Set required fields
}
|
Also note that tests should never rely on default values that are specified by a helper method since that forces readers to read the helper method’s implementation details in order to understand the test.
// This test needs a public company, so explicitly set it.
// It also needs a company with no board members, so explicitly clear it.
Company publicNoBoardMembers = newCompany().setType(PUBLIC).clearBoardMembers().build();
|
You can learn more about this topic at http://www.natpryce.com/articles/000714.html
So, we don't set defaults? Your advice seems contradictory there.
OdpovedaťOdstrániťNo. Set defaults, but make your test set those same values if it cares about that particular value. It's about making your test inputs easy to create but not relying on that state in the test itself. Basically, you should be able to change the defaults at any time for any reason to anything and your tests should still pass.
OdpovedaťOdstrániťI think it would be better not setting defaults in order to force developers to explicitly set values in their tests.
OdstrániťWe don't want to make our tests robust, but our production code, if you know what I mean...
It seems valid to initialize the object with sensible values and then explicitly re-declare the things you care about in the body of the test.
OdpovedaťOdstrániťDo you have recommendations on techniques for handling the case of inadvertent inclusion? Ie, the test inadvertently becomes dependent on a default unintentionally. ("oh yeah, this is always initialized to , so ...")
Perhaps some sort of "change every default/inverse" antimatter test object to validate you're not depending on the defaults. Bonus would be getting to include "antiMatter[inputName]" as test code.
What we do is create defaults that would normally be 'good' results or uninteresting golden path tests, and then allow tests to add nondefaults when they want to test the variation of the behavior. This allows for the tests to be very clear on what is changing and why with a minimum of repetition. You can easily see what input is changing and what matters.
OdstrániťI think the point here is if we have defaults that influence the test outcome we should at least make this visible to the testing framework users. Not simply hiding it under the hood.
OdpovedaťOdstrániťE.g. Lets say that Company behavior is influenced by its number of employees (small, medium, big, being small less than 6).
You could then include some knowledge in the method names of where you create the builder.
private static Company.Builder newCompanyForSmallNumberOfEmpoyees() {
return Company.newBuilder().setType(PUBLIC).setEmployees(5); // Set required fields
}
On usage:
Company smallCompany = newCompanyForSmallNumberOfEmpoyees().build();
What would you recommend in case of Kotlin's default parameters?
OdpovedaťOdstrániťIs having build pattern still beneficial?