📜 ⬆️ ⬇️

Using junit-quickcheck on a simple example in the practice of TDD

Once again, practicing TDD in Java, I created a small training project in which I showed that if the tests pass, this does not mean that the code is executed correctly. In fact, the development of unit tests that take into account the various options for input data is a hard work. To solve this problem, I found the junit-quickcheck library for myself, and I am in a hurry to share my experience of using it.

By the way, on Habré about this library already have a great article , but I use another example of the code being tested.

The first steps


So, I created a new empty Java project and made an assembly script. I build this project using gradle. You can see my study project here . Starting from this commit: d9bd97eaec20427e725f9a4c3ff0c0d36cc27ea3 the project is ready for building and implementing our test case.

Now I have to start writing code following the TDD practice. Some TDD apologists consider it appropriate to begin class development by writing a test for this class. But here I would argue with them. My IDE (IntelliJ IDEA) is able to generate tests in the presence of a ready-made class, so I will start the development by creating an empty class :
')
public class HelloSayer { } 

Here and below, the code is illustrated with links to a commit on github.

And now I will ask my IDE to create a test for me and then I will start working according to the test-first scheme. The first test I will do is a very simple test, with which I simply create a new object. I want to specify with whom I need to say hello in the constructor. Moreover, I believe that the object issuing the greeting can issue this greeting to only one. Therefore, this class will not have a constructor without a parameter.

Here is the first test I created:

  @Test public void testCreating() throws Exception { new HelloSayer("World"); } 

Note that at this point the project is not compiled, that is, the test is red.

Well, I'll make the test green. My IDE can create class methods (including constructors), which I already use, as in the created test, but have not yet implemented. Using one key combination, I generate this constructor and get a green test:

 public class HelloSayer { public HelloSayer(String whom) { } } 

Please note that the test, of course, passes, but in fact, my class does not do what I wanted from it. We work on.

I create a test for the getWhom method - this is a getter, with the help of which I will be able to know to whom this object will issue a greeting:

  @Test public void testWhomGetter() throws Exception { HelloSayer sayer = new HelloSayer("World"); assertEquals("World", sayer.getWhom()); } 

The project is not compiled again, that is, the test is again red.

We continue.
Implement the required getter:

 public class HelloSayer { private String whom; public HelloSayer(String whom) { } public String getWhom() { return whom; } } 

The project is already compiled, but the test fails. In order to pass the test, we now have to implement the designer for real, and not in the way I did before:

  public HelloSayer(String whom) { this.whom = whom; } 

The test is green again.

A small but important explanation. In this case study, I commit to the repository very often. I do this in order to show the reader the sequence of my steps. Making commits so often in a real project is not necessary.

Although I am in favor of the commit being “atomic”. What is an atomic commit, you can read, for example, here: seesparkbox.com/foundry/atomic_commits_with_git . Crato to say: as long as you can formulate what you have done, in one sentence it is an atomic commit.

Incorrect implementation green test


Well now, let's implement the main method of this class: getting a welcome message.
Here is the test:

  @Test public void testGreetingString() throws Exception { HelloSayer sayer = new HelloSayer("World"); assertEquals("Hello \"World\"", sayer.getGreetingString()); } 

I expect that the name of the person to whom my greeting writer refers to will be quoted. The test is red.

We implement the required method:

  public String getGreetingString() { return "Hello \"World\""; } 

The test is green. I achieved passing the test very easy! By the way, when implementing the getWhom method I could do the same, but that time I did everything honestly, but I was too lazy here.

So, we have a problem: tests all pass, but the class does not do what we need. One can argue here: it is unlikely that such a problem may arise in real life, and not in an educational project. In fact, unit tests are usually developed by the same person who writes the functionality. Therefore, it would be very strange if this person, wrote the implementation of the method, which so does not meet the requirements, although formally, it corresponds to the test.

But, secondly, there are cases when different people write tests and functionality. And first of all, there are much more complicated cases when a developer may simply not provide for all possible input data in tests.

In fact, in order to avoid such an incorrect implementation, it would be enough to make a second test with another string of whom . But I will try to resolve this issue for the most general case. Solution of the problem in general form requires the use of many different lines. That is, it is necessary to conduct a test not on the same line of the “World”, as I did, but on different lines.

To solve the problem in general, I used a third-party junit-quickcheck library: github.com/pholser/junit-quickcheck , which is built on the basis of Theories from JUnit. I connect it to my project.

  dependencies { testCompile 'junit:junit:4.+' + testCompile 'com.pholser:junit-quickcheck-core:0.5+' + testCompile 'com.pholser:junit-quickcheck-generators:0.5+' } 

At the time of this writing, version 0.5, which I use here, has the status of alpha, but it claims Java 8 support, which I use for this project.

Using this library, I re- implemented the test method to test the greetingString method:

  @Theory public void greetingString( @ForAll String whom ) { HelloSayer sayer = new HelloSayer(whom); assertEquals(String.format("Hello \"%s\"", whom), sayer.getGreetingString()); } 

We will understand what this code does. The Theory annotation with the method indicates that this is a parameterized test method built on the basis of therories. Annotation ForAll shows that this parameter will be generated. junit-quickcheck out of the box can generate values ​​for many types of data, including for strings.

We start the test and now it is red, as we need. Now I will correct my implementation of the getGreetingString method:

  public String getGreetingString() { return String.format("Hello \"%s\"", whom); } 

Now the test is green and the implementation is, in fact, as it should be. I recommend to put a breakpoint and trace what parameters are passed to this method. I would not invent such lines.

The next step I rewrote all the tests using string generation.
Generally speaking, this is also a controversial issue. Perhaps it should leave a simple test, with a simple, understandable line. If you have to debug your code (for example, if you get unexpected behavior on some specific input line) then it would be more convenient to have a simple test. Debugging the implementation with the generator is not very easy, since the test code is called repeatedly. It is much easier to implement a simple test that uses a strictly fixed string that you need. But I, nevertheless, replaced all the tests with tests with generated parameters.

Small final line of this section, I added control of code coverage to my project. I use for this Jacoco. I banish, watch the report and enjoy 100% code coverage.

Testing different implementations


Now we are starting to develop our “very important” class HelloSayer . I do not really like its implementation.
Specifically, I don’t like String.format every time the getGreetingString method is called . However, there are situations when it will be OK, and there are situations when it will not suit me.

Therefore, I will render the HelloSayer interface and make several implementations of it. Here, I again used the capabilities of IntelliJ IDEA on refactoring. That's what I got as a result. Now HelloSayer became the interface, and the implementation that was there went to the HelloSayerInplace class.

Interface and implementation
Interface:
 package info.risik.books.tdd.HelloWorld; public interface HelloSayer { String getWhom(); String getGreetingString(); } 

Implementation:

 package info.risik.books.tdd.HelloWorld; public class HelloSayerInplace implements HelloSayer { private String whom; public HelloSayerInplace(String whom) { this.whom = whom; } @Override public String getWhom() { return whom; } @Override public String getGreetingString() { return String.format("Hello \"%s\"", whom); } } 


And now I’ll make another implementation of this interface, which creates a welcome string right in the constructor and stores it in the object. I also implemented the test, thanks to copy-and-paste and search-and-replace:

Another implementation of the interface
Implementing the HelloSayerAtOnce class.

 package info.risik.books.tdd.HelloWorld; public class HelloSayerAtOnce implements HelloSayer { private String whom; private String message; public HelloSayerAtOnce(String whom) { this.whom = whom; this.message = String.format("Hello \"%s\"", whom); } @Override public String getWhom() { return whom; } @Override public String getGreetingString() { return message; } } 

Unit test for it:

 package info.risik.books.tdd.HelloWorld; import com.pholser.junit.quickcheck.ForAll; import junit.framework.TestCase; import org.junit.contrib.theories.Theories; import org.junit.contrib.theories.Theory; import org.junit.runner.RunWith; @RunWith(Theories.class) public class HelloSayerAtOnceTest extends TestCase { @Theory public void testCreating( @ForAll String whom ) { new HelloSayerAtOnce(whom); } @Theory public void testWhomGetter( @ForAll String whom ) { HelloSayer sayer = new HelloSayerAtOnce(whom); assertEquals(whom, sayer.getWhom()); } @Theory public void greetingString( @ForAll String whom ) { HelloSayer sayer = new HelloSayerAtOnce(whom); assertEquals(String.format("Hello \"%s\"", whom), sayer.getGreetingString()); } } 


There is a problem here. I want another third implementation of the same interface, and accordingly, I will have to make a third test class, again through the same copy-and-paste and search-and-replace mechanism. This is bad. All classes that implement this interface, regardless of how they are implemented internally, must comply with the same agreement, which I have already described in the test. I want to follow the principle of DRY (don't repeat youself). That is, I want to apply the same test to all possible implementations of this interface.

Here junit-quickchecker came to my rescue again. Here is a modified test:

 @RunWith(Theories.class) public class HelloSayerTest { enum HelloSayerType { InPlace, AtOnce, } //... @Theory public void testWhomGetter( @ForAll String whom, @ForAll @ValuesOf HelloSayerType sayerType ) throws Exception { HelloSayer sayer = getFactory(sayerType, whom); assertEquals(whom, sayer.getWhom()); } //... private HelloSayer getFactory(HelloSayerType type, String whom) throws Exception { switch (type) { case InPlace: return new HelloSayerInplace(whom); case AtOnce: return new HelloSayerAtOnce(whom); } throw new Exception("Unknown HelloSayerType"); } } 

I added one generated parameter to the test method: class type (as an enumeration). Enumerations in junit-checker are supported out of the box. To do this, I just need to add the @ValuesOf annotation. And, in fact, all I needed to do was to create an instance of one of the classes.

Re-implement all test methods using this parameter. I no longer need a separate test class for the new class, I delete it.

And for a snack, I implement the third version of HelloSayer:

Implementing HelloSayer with lazy string initialization
 package info.risik.books.tdd.HelloWorld; public class HelloSayerLazy implements HelloSayer { private String whom; private String message; public HelloSayerLazy(String whom) { this.whom = whom; this.message = null; } @Override public String getWhom() { return whom; } @Override public String getGreetingString() { if (message == null) { makeMessage(); } return message; } private void makeMessage() { message = String.format("Hello \"%s\"", whom); } } 


Now, to implement all the tests for this class, I need to add only 3 lines:

  enum HelloSayerType { InPlace, AtOnce, + Lazy, } @Theory @@ -53,6 +54,8 @@ private HelloSayer getFactory(HelloSayerType type, String whom) throws Exception return new HelloSayerInplace(whom); case AtOnce: return new HelloSayerAtOnce(whom); + case Lazy: + return new HelloSayerLazy(whom); } throw new Exception("Unknown HelloSayerType"); } 

Check that the code coverage is still 100%. All perfectly!

Links and other necessary information


Java test cases : github.com/risik/tdd-book-java
Text of the original article: github.com/risik/tdd-book/blob/master/helloworld/simple_test.ru.md
The text of the article prepared for habrahabr.ru: github.com/risik/tdd-book/blob/master/helloworld/simple_test.ru.habr.txt
License: CC-BY-NC-SA

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


All Articles