Is it possible to wet the object under test?
Yes:
-
Class to test
class Message extends Data { protected $text = ''; public function check() { return empty($this->text); } public function setText($text) { if ($this->check() === true) { $this->text = $text; } else { $this->text .= $text; } } }
- Developers Vasya and Petya , who are arguing how to write
a test for
setText
correctly, do you need to use thecheck
method or not?
Vasya: for mock check
During unit testing, you need to do everything possible except for hidden data (private, protected
) to test all scenarios that do not depend on each other. In total, there will be 3 scenarios, where the IOC check
returns either true
, false
or otherwise, for the setText
method. And test the check
method separately. Also forget the component test, which will test the object (Message
) in general, that is, the interface of this object, without touching anything that concerns Message
and Data
.
Petya: against mock check
You can't mix methods of the class under test (Message
) and its parents (Data
), only external classes are allowed. If you change the check
method, setText
will actually break, but the unit will not report it. Themselves the units will show how the class works, it is unnecessary to do component tests on only one object, but the tests themselves will duplicate the check of the same internal dependencies (the check
method will be tested 2 times)
Question:
Which of them is right, or how to test correctly?
Noted:
As usual in my questions, this is a training question. There is no problem with the specific code, this is just an example (it is better to understand the examples)
3 answers
For the idea of moking the test class, I would immediately shoot. The class under test is a single entity, and should be tested as a single entity. By crawling your dirty hands into the check
method, you are assuming a concrete implementation of the setText
method. This is not a unit test, this is profanity: you are not testing the external interface of the class, but checking that the operations in it are written exactly the same as in the test.
Where the class interface says that setText
should call check
, and not cache the state to some variable? Now you can't change the implementation while maintaining the interface and behavior without breaking the unit tests. So why do unit tests that break down from changing the internal implementation? They will only annoy you.
But if some methods of the tested class will be difficult to test (they will climb on external resources, for example), then you can already think about mockups. But in this case, most likely, the logic of climbing on external resources should be allocated to a separate class and wet it already.
I generally support the ideas of Martin Fowler: you only need to wet what is too difficult not to wet: databases, mail distribution, etc. This position he calls " classical TDD ".
Every time you stick an ioc into the code you're testing, you're spoofing the actual behavior, you're assuming the implementation, you're simulating the behavior, and you're making mistakes. Your code is not testing a real system, but a simulation.
Yes, tests with a minimum of mocs break more often armfuls, not singly. But they break down! And if they break, you can immediately see that there is a real error somewhere. If the tests test the implementation, they break one by one, but common errors for several classes are not caught, and failed tests often simply indicate a change in the implementation.
"Mockers" like to produce miniature unit tests for each getter and setter, but they forget that classes do not exist in an airless space, but interact. As a result, everyone has the class coverage is 99%, and together for some reason everything breaks down. We must not forget about integration tests. And unit tests that are "slightly integrative" are a great way to go.
Don't be afraid to call a method from a dozen different places: the more real calls, the more confidence you have in the reliability of the system.
As for your code specifically, your method looks like a setter, but in fact it is not. This is a test failure before writing any tests.
In this particular case, I would change the interface. The setText
method looks like a normal setter, but it's not really a setter.
It looks very much like it should be broken down into methods clearText
and appendText
, and take out the logic hidden behind check
above.
Vasya's dispute with Petya indirectly indicates problems in the project. Petit's position is stronger (you can't mix the methods of the class under test), because it follows the decomposition rules described in Uncle Bob Martin's book "Clean code".
In short: methods of the same logical level should be located on the same decomposition layer, such code is much easier to read. In your example, it looks like check
should be located somewhere higher, where it should be tested. And there you can create a class Message
, which is one level lower.
And here, at the Message
level, we can test the clearText
and appendText
methods, possibly by passing some classes below.
P.S. I note that in real projects, there is no it is always possible to correctly position entities by levels, so Vasya and Petya risk discussing this issue until they are hoarse.
P. P. S. Noticed that we are talking about mocks. I follow this rule whenever possible: first, test directly, if it does not work, and you can not reverse engineer, use the stub, if it does not work, use the ioc. Thus, simple validating or transforming methods that work with standard library objects (strings, dates, regular expressions) flow into utility classes, where they are tested.
Your class doesn't have a full API. Add the getText
method and test the behavior comprehensively.
Alternatively, you can use the following tests:
- On a clean object, make sure that
check
returnsfalse
. - On a clean object, make sure that
getText
returns''
. - After
setText
, thecheck
method returnstrue
. - After
setText
, thegetText
method returns the correct string. - After repeated
setText
, thegetText
method returns the correct string.
A small digression:
Some methods are very difficult to test in isolation from each other, and this is normal. A typical example is a container:
class Container {
private $data;
public function get(key) {/* ... */}
public function set(key, value) {/* ... */}
}
The get
and set
methods simply cannot be tested separately from each other. In such cases, it makes sense to test the behavior of the object (or a group of methods).
In addition, it is more correct to approach the methods as small "black boxes". Tests in neither in no case should they depend on the implementation of the method. In your example, if you replace the check
call with something else, your tests will break, although the behavior of the method will remain the same.
Testing should not be an end in itself. Therefore, it is wrong to use some clever techniques (moki of the object under test, inheritance, in order to disclose the structure, etc.) in most cases. Tests are just a tool to ensure that the code works.