📜 ⬆️ ⬇️

Readable test

Introduction

This article was written as a result of my repeated meetings on viewing code with anti-patterns of writing not very readable tests, not the last role in which incorrect work with test data plays. In this article, I will explain the theory of the readable test and show how to achieve the identified characteristics through thoughtful naming and competent application of rendering auxiliary methods.

Unit What it is?

Unit testing is usually translated into Russian as unit testing. However, the word “module” has a slightly different connotation of color associated with the deployment pattern. Therefore, in order to avoid unnecessary associations, we will use Anglicism "unit". Once again, remember what a unit is in terms of unit testing terminology:

A unit is a piece of code that, in a given environment, gives certain output for certain input data.

unit definition
Note that in addition to the unit itself, all the other components of this definition can be degenerated into an empty set, but the more empty participants in this mess, the less meaning (semantics) is contained in the unit.

')
Examples
In order not to be sacrificed to Moloch of abstraction, in the words of Nietzsche, let us look at a couple of examples that vividly demonstrate each of the components of this love square.

Empty surroundings
This is the most common configuration for units written in the style of structural decomposition. A function that takes arguments and returns a result is an example of a unit that has many arguments as input data and a result as output data . The connection of the result with the input parameters is the essence of the function. For example, the function y = sin (x) is a mapping of the set of all real numbers on the interval [-1,1]. For each input x, y is uniquely determined, and the responsibility of the sin (x) function is to ensure this uniqueness. sin (x) will display these x in these y in any environment for any user, at any time of the year. He is not interested in the environment.

Blank output
Often, the responsibility of a unit is not just to return something, but to do something with a certain state. For example, the classic mediator pattern assumes the presence of an object ( unit ), which receives certain events ( input data ) and calls some methods on a set of objects ( environment ). This object does not return anything to anyone - its responsibility to provide invariants between the set of objects that it is intended to link. Providing these invariants is his responsibility. The influence of input events on the environment is the definition of a unit mediator. A mediator window that activates a section of the dialogue after a click on the radio button is a specific example of units of this configuration.

Blank Input
The necessity of the existence of such units follows from the simple logical conclusion that somewhere the data should still arise, and not be submitted from the outside. A good example of such a unit is the GetCurrentTime () ( unit ) function, which reads the current time from the system ( environment ) and returns it ( output data ) to the client.

void f ()
A function that does not accept anything, returns nothing and does not interact with the environment in any way has little meaning. This is a certain thing in itself that it is better not to disturb — not to use and not to create at all.

State and units
In the light of the above examples and definitions, we look at the void std :: list :: remove (const T & elem) function. Suppose elem is input. But what about the environment and the output? No output - void function. And the environment then what? Formally, memory. But memory is the essence of a lower level than the list. After all, no one will argue that "removal from the list is the release of memory according to some cunning scheme." If you try to determine what it means (what semantics it has) “removal from the list”, then it will sound like this: “deleting an element from the list is changing the state of the list so that it is no longer possible to get this element from the list”. So, the definition of the deletion semantics from the list is made on the same level of abstraction as the deletion itself, using another operation from the list interface. Thus, the list :: remove function itself is not a unit . The function remove itself, in isolation from everything, cannot be determined (and verified). The list makes sense only in the totality of operations on it. That list is a unit . The input to it is a set of pairs (command, command_arguments), and the output of a pair (query, query_results). By the way, this STL design corresponds to the principle of CQS (command query separation), in which the methods that change state and return it are separated from each other (this is done, for the sake of safety, to exceptions). In the general case, however, for a class, the set of output data can also use the results of some of its operations that have input data. Bad, but possible. By the way, the question is, what about the environment? Does std :: list also have environment agnostic? Not! You can submit allocator to it! Working with memory is a list through the alokator. The list clearly declared its dependence on the environment. Well done!

Readable Tests

It would seem, what does the theory of unit tests have to do with when they promised to talk about the readability of tests? The answer is simple:

The unit test is readable, in which all four components involved in determining a unit: a unit , input and output, and the environment are obvious.

Speaking of obviousness, it is meant that an outsider, possessing only the context of the subject area in which the test is written, reading only the test and not being forced to be distracted by finding out the details in another context, can guess the essence of the test and the statements and tests it makes. If this is not the case, then the test is not readable and needs to be improved - often with the refinement of the interface and design of the unit.

Test anatomy

Before giving specific recommendations, we make some definitions so that there is no ambiguity. So, the type test written using the gtest unit testing framework looks like this:
TEST(Subject, Assertion) { // Body } 

or so:
 TEST_F(Subject, Assertion) { // Body } 

rules


Unit visibility
Rule: Subject must unequivocally indicate a unit or system under test.
Explanation: Subject must be a noun of the English language (preferably non-composite and short), denoting a unit. All tests in this file must have this unit with their Subjects, and the mix of TEST and TEST_F macros with the same Subject is prohibited.
Examples:

Poorly:
 TEST(GetDiskSignature, ReadsFirstSector) TEST(SetDiskSignature, WritesToFirstSector) 

Good:
 TEST(DiskSignature, ResidesOnFirstSector) 

Test predicative
Rule: The Assertion must contain a full statement (a real-time narrative sentence) in English, subject to a meaningful check.
Explanation: From reading the statement, the reader should have some expectation from the test contents. That is, after reading this Assertion about a given Subject, the reader should have a plan in his head, as if he himself verified this statement. That is why parasitic words such as “correctly”, “good”, “fine”, “well” and similar ethical-moral epithets are strictly forbidden as a predicate predicate. The essence of the test is just to reveal what it means to "correct", "good", etc. Such words in the predicate are allowed excluded as determinable entities, and not as definitions themselves.
Examples:

Poorly:
 TEST(FileCache, IsInitializedCorrectly) 

Good:
 TEST(FileCache, IsInitializedAsEmpty) 


Using Entities from Assertion to Body
Rule: Identifiers used in the test body should reuse the terms used in the statement.
Explanation: What could be more obvious to the reader when reading a test than literal repetition of the terms from the statement? If the reader was made some quality promise in Assertion and he had some expectations that he would be in the test, nothing would help him to cling to the used entities as a repetition of the terms from the statement. Small modifications are allowed, such as changing the case, numbers and even using any short suffixes and prefixes - the human brain (especially Russian) very effectively fights with such transformations of symbols, considering them equivalent to the original symbol. The more mutations in the term, the harder it is to recognize the reader. Therefore, even synonyms are not welcome.
Examples:

Poorly:
 TEST_F(MRUCache, MovesLastAccessedItemToFront) { Items.Touch("http://facebook.com/"); Items.Touch("http://habrahabr.ru/"); EXPECT_EQ(0, Items.GetIndex("http://habrahabr.ru/")); } 

Good:
 TEST_F(MRUCache, SetsIndexOfLastTouchedItemToZero) { MRUCache.Touch(Item("http://facebook.com/")); MRUCache.Touch(Item("http://habrahabr.ru/")); EXPECT_EQ(0, MRUCache.GetIndex(Item("http://habrahabr.ru/"))); } 

Good:
 TEST_F(MRUCache, MovesLastAccessedItemToFront) { MRUCache.Access(Item("http://facebook.com/")); MRUCache.Access(Item("http://habrahabr.ru/")); EXPECT_EQ(Item("http://habrahabr.ru/"), MRUCache.Front()); } 


Implicitness of test data
Rule: All test data (both input and output), exactly as the data read from the environment or written in the environment, must be present in the body of the test.
Explanation: When reading, the first thing that a third-party reader clings to is a domain expert - this is familiar data. The person from the cradle builds his knowledge from the particular to the general, therefore the person perceives the particular specifics best of all else. It is well-chosen data collected in one context that achieves the greatest expressiveness of the test. This is such an important aspect of the readable test that anti-patterns of violation of this rule will be placed in a separate article and alternatives to combat them will be shown.
Examples:

Poorly:
 const Common::String SomeUnixPath = GET_WCHAR("/var/log"); const int SomeUnixPathComponents = 2; ... TEST(UnixPath, ContainsSlashes) { EXPECT_EQ(SomeUnixPathComponents, Paths::Unix(SomeUnixPath).Components()); } 

Good:
 Common::String Path(const char* value) { return GET_WCHAR(value); } ... TEST(UnixPath, ContainsSlashes) { EXPECT_EQ(2, Paths::Unix(Path("/var/log")).Components()); } 


Transparency of test data flow
Rule: The test body must contain all auxiliary objects participating in the data stream through the unit.
Explanation: When writing unit tests, it is customary to take frequently repeated operations (and this is good) into auxiliary functions with expressive names, but when using the TEST_F macro, there is often a pernicious tendency to use fixture members in these functions without explicitly passing them. In the end, even though these members eventually get to the unit being tested, they can learn that the test somehow influenced their contents only by going to the auxiliary function and reading its code. That is, a test for its understanding requires a transition to another context, which is a deterioration of readability. Therefore, even if the auxiliary function can access the test data in other ways, they must still be explicitly given out of the test body in order to show the data flow and its effect on the unit's behavior.
Examples:

Poorly:
 MockFileSystem Files; void AddFile(std::string path, int size) { ON_CALL(Files, Get(path.c_str()).WillByDefault(Return(File(size))); } ... TEST(FileStatistics, SumsFileSizes) { AddFile("/bin/ls", 10); AddFile("/bin/bash", 20); EXPECT_EQ(30, GetStats(Files, "/bin").Size); } 

Good:
 MockFileSystem Files; void AddFile(MockFileSystem& fs, std::string path, int size) { ON_CALL(fs, Get(path.c_str()).WillByDefault(Return(File(size))); } ... TEST(FileStatistics, SumsFileSizes) { AddFile(Files, "/bin/ls", 10); AddFile(Files, "/bin/bash", 20); EXPECT_EQ(10 + 20, GetStats(Files, "/bin").Size); } 


Conclusion

This article discusses the theory and practice of writing readable tests. The above examples do not exhaust the whole range of tricks that make the test better - these are just the husks that I noticed in my tests, just like in other people's viewed tests. The basic rule is to write a self-sufficient test. If anything is put into an auxiliary function, then the name of the function itself, exactly like its signatures, should be so obvious that there is no need and desire to watch its implementation. Take out to hide the irrelevant and name the essential!

Source: https://habr.com/ru/post/186666/


All Articles