https://testing.googleblog.com/2018/02/testing-on-toilet-cleanly-create-test.html
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
http://www.natpryce.com/articles/000714.html
https://testing.googleblog.com/2018/06/testing-on-toilet-only-verify-relevant.html
These arguments may need to be updated when the code under test is changed, even if the changes are unrelated to the behavior being tested. For example, if additional text is added to TitleBar, every test in the codebase that specifies this argument will need to be updated.
In addition, verifying too many arguments makes it difficult to understand what behavior is being tested since it’s not obvious which arguments are important to the test and which are irrelevant.
Instead, only verify arguments that affect the correctness of the specific behavior being tested. You can use argument matchers (e.g., any() and contains() in Mockito) to ignore arguments that don't need to be verified:
@Test public void displayGreeting_showSpecialGreetingOnNewYearsDay() {
fakeClock.setTime(NEW_YEARS_DAY);
userGreeter.displayGreeting();
verify(mockUserPrompter).updatePrompt(contains("Happy New Year!"), any(), any()));
}
|
Arguments ignored in one test can be verified in other tests. Following this pattern allows us to verify only one behavior per test, which makes tests more readable and more resilient to change. For example, here is a separate test that we might write:
@Test public void displayGreeting_renderUserName() {
fakeUser.setName(“Fake User”);
userGreeter.displayGreeting();
// Focus on the argument relevant to showing the user's name.
verify(mockUserPrompter).updatePrompt(contains("Hi Fake User!"), any(), any());
}
|
https://testing.googleblog.com/2017/01/testing-on-toilet-keep-cause-and-effect.html
write tests where the effects immediately follow the causes
https://testing.googleblog.com/2014/10/testing-on-toilet-writing-descriptive.html
Putting both the scenario and the expected outcome in the test name
https://testing.googleblog.com/2015/01/testing-on-toilet-prefer-testing-public.html
https://testing.googleblog.com/2014/04/testing-on-toilet-test-behaviors-not.html
it can be harmful to think that tests and public methods should have a 1:1 relationship
It's a much better idea to use separate tests to verify separate behaviors:
This will make your tests more resilient since adding new behaviors is unlikely to break the existing tests, and clearer since each test contains code to exercise only one behavior
https://testing.googleblog.com/2013/08/testing-on-toilet-test-behavior-not.htmlTests that are independent of implementation details are easier to maintain since they don't need to be changed each time you make a change to the implementation. They're also easier to understand since they basically act as code samples that show all the different ways your class's methods can be used, so even someone who's not familiar with the implementation should usually be able to read through the tests to understand how to use the class.
Note that test setup may need to change if the implementation changes (e.g. if you change your class to take a new dependency in its constructor, the test needs to pass in this dependency when it creates the class), but the actual test itself typically shouldn't need to change if the code's user-facing behavior doesn't change.
Translated to English: “(1) I had $5 and was able to withdraw $5; (2) then got rejected when overdrawing $1; (3) but if I enable overdraft with a $1 limit, I can withdraw $1.” If that sounds a little hard to track, it is: it is testing three scenarios, not one.
A better approach is to exercise each scenario in its own test:
Writing tests this way provides many benefits:
- Logic is easier to understand because there is less code to read in each test method.
- Setup code in each test is simpler because it only needs to serve a single scenario.
- Side effects of one scenario will not accidentally invalidate or mask a later scenario’s assumptions.
- If a scenario in one test fails, other scenarios will still run since they are unaffected by the failure.
- Test names clearly describe each scenario, which makes it easier to learn which scenarios exist.
One sign that you might be testing more than one scenario: after asserting the output of one call to the system under test, the test makes another call to the system under test.
While a scenario for a unit test often consists of a single call to the system under test, its scope can be larger for integration and end-to-end tests.
Unlike production code, simplicity is more important than flexibility in tests. Most unit tests verify that a single, known input produces a single, known output. Tests can avoid complexity by stating their inputs and outputs directly rather than computing them.
After eliminating the unnecessary computation from the test, the bug is obvious—we're expecting two slashes in the URL! This test will either fail or (even worse) incorrectly pass if the production code has the same bug. We never would have written this if we stated our inputs and outputs directly instead of trying to compute them. And this is a very simple example—when a test adds more operators or includes loops and conditionals, it becomes increasingly difficult to be confident that it is correct.
Another way of saying this is that, whereas production code describes a general strategy for computing outputs given inputs, tests are concrete examples of input/output pairs
When tests do need their own logic, such logic should often be moved out of the test bodies and into utilities and helper functions. Since such helpers can get quite complex, it's usually a good idea for any nontrivial test utility to have its own tests.
https://testing.googleblog.com/2017/11/obsessed-with-primitives.html
Code Health: Obsessed With Primitives?
However, code that relies too heavily on basic types instead of custom abstractions can be hard to understand and maintain.
Primitive obsession is the overuse of basic ("primitive") types to represent higher-level concepts. For example, this code uses basic types to represent shapes:
Replacing basic types with higher-level abstractions results in clearer and better encapsulated code:
It's possible for any type—from a lowly int to a sophisticated red-black tree—to be too primitive for the job
Primitive obsession is the overuse of basic ("primitive") types to represent higher-level concepts. For example, this code uses basic types to represent shapes:
vector<pair<int, int>> polygon = ...
pair<pair<int, int>, pair<int, int>> bounding_box = GetBoundingBox(polygon);
int area = (bounding_box.second.first - bounding_box.first.first) *
(bounding_box.second.second - bounding_box.first.second);
|
pair
is not the right level of abstraction because its generically-named first
and second
fields are used to represent X and Y in one case and lower-left (er, upper-left?) and upper-right (er, lower-right?) in the other. Worse, basic types don't encapsulate domain-specific code such as computing the bounding box and area.Replacing basic types with higher-level abstractions results in clearer and better encapsulated code:
Polygon polygon = ...
int area = polygon.GetBoundingBox().GetArea();
|
Here are some other examples of primitive obsession:
- Related maps, lists, vectors, etc. that can be easily combined into a single collection by consolidating the values into a custom higher-level abstraction.
map<UserId, string> id_to_name; map<UserId, int> id_to_age;
map<UserId, Person> id_to_person;
- A vector or map with magic indices/keys, e.g. string values at indices/keys 0, 1, and 2 hold name, address, and phone #, respectively. Instead, consolidate these values into a higher-level abstraction.
person_data[kName] = "Foo";
person.SetName("Foo");
- A string that holds complex or structured text (e.g. a date). Instead, use a higher-level abstraction (e.g.
Date
) that provides self-documenting accessors (e.g.GetMonth
) and guarantees correctness.string date = "01-02-03";
Date date(Month::Feb, Day(1), Year(2003));
- An integer or floating point number that stores a time value, e.g. seconds. Instead, use a structured timestamp or duration type.
int timeout_secs = 5;
Duration timeout = Seconds(5);
- Use of primitives instead of small objects for simple tasks (such as currency, ranges, special strings for phone numbers, etc.)
- Use of constants for coding information (such as a constant
USER_ADMIN_ROLE = 1
for referring to users with administrator rights.) - Use of string constants as field names for use in data arrays.
Code Health: Reduce Nesting, Reduce Complexity
The refactoring technique shown above is known as guard clauses. A guard clause checks a criterion and fails fast if it is not met. It decouples the computational logic from the error logic. By removing the cognitive gap between error checking and handling, it frees up mental processing power. As a result, the refactored version is much easier to read and maintain.
Here are some rules of thumb for reducing nesting in your code:
Here are some rules of thumb for reducing nesting in your code:
- Keep conditional blocks short. It increases readability by keeping things local.
- Consider refactoring when your loops and branches are more than 2 levels deep.
- Think about moving nested logic into separate functions. For example, if you need to loop through a list of objects that each contain a list (such as a protocol buffer with repeated fields), you can define a function to process each object instead of using a double nested loop.