Intro
This is simple Hello World Java application that successfully combines Spring Framework with Mockito and Lombok. It's just for demonstrative purposes, so the presented example is very simple and used many times before in the literature.The final goal is to show up how simple is to combine some of the best features of each involved technology with Test-Driven-Development in mind:
- Spring Framework: wiring
- Mockito: isolation
- Lombok: simplicity
Example
A text editor uses and spell checker among other things to make the life of user easier. Depending on the target language, an spell editor may change is behavior and its dependencies as well, for example dictionaries and tokenizers. Simplifying the whole idea you may encounter a contract-driven (interface based) solution like this:with many possible combinations of spell checkers, dictionaries and tokenizers. For example this one:
where every contract was replaced by a concrete instance. Combinations are multiple, and sometimes you cannot predict all them in a classes/interfaces design.
You want to start engaging TDD for sure, and you want to do it in a top-down fashion. That means, start testing the text editor first, then the spell checker, then the remaining pieces. You also want to do it in isolation, so spell checker and the remaining implementations aren't involved in editor tests, and it's your desire to obtain zero infrastructure code (no constructor calls). As a plus no getter nor setter nor trivial constructor implementations.
What to test?
Our test will be very simple as well, here is the top-down ordered list (remember it's a simplification of the reality):1- Given an Editor, when a new text is pasted it should be added.
2- Given an Editor, when a new text is pasted the spell should be checked against the appended text.
3- Given an English spell checker, when a word is not found in a dictionary during the spell check then it must be signaled as misspelled and returned back.
You may be wondering, why should I test these obvious use cases? Trust me, it's very important to cover all possible scenarios if you really want to delivery software with built-in quality. This is the hidden synergy behind TDD.
Hands on Spring Tool Suite
- Open STS and create a new Maven project.
- Check 'Create a simple project (skip archetype selection)'.
- Enter a Group Id = demos.sf.editor, Artifact Id = spring-hello-world and Version = 1.0.
- Edit your pom.xml and append all the required dependencies. It should ends like:
- Start testing! A golden rule, don't forget it. Create a new JUnit 4 Test Case at src/test/java. Name it EditorTest in package demos.sf.editor.
- Delete the existing test, called test and append a new failing test for the first use case. Its name testPasteSuccess:
- Write the assertion first. How's that? See use case 1: "... the text should be added":
- Right-click on editor and choose "Create local variable...". Change its type from Object to Editor, a non-existing class:
- Annotate the Editor class for auto-implementing the getter using Lombok annotations:
- Go back to the test, and perform the call to a non-existing paste() method:
- Ctrl+1 on top the editor.paste("...") call and choose "Create method paste()....". A new method is generated:
- In the test, the only missing thing is the editor initialization. Let's inject the Editor at test time. Convert the editor local variable to a field declaration and annotate it as @Autowired:
- Create a new Spring Bean Configuration file at src/test/resources/test-applicationContext.xml and declare the editor bean on it:
- Use the Spring test runner and load the application context via annotations:
- Right click on test case and Run As JUnit test. The test will fail but corner stones are ready to support more agile tests. Make the test pass by implementing the paste() method. Oops! remember to remove the fail() from the test.
public class Editor { @Getter private String text; public void paste(String cut) { if(text == null) { text = ""; } text += cut; } }
- Re-run the test, this time also using a Maven run configuration like this:
- The spring default behavior is to cache the application context between different tests in the same test case. They also introduced the annotations @TestExecutionListeners and @DirtiesContext to mark the application context as dirty after each test:
- Now starts the use case 2. First append the test in failing mode:
- Using Mockito style, we need to verify that the spell is checked against the pasted text. The syntax is straightforward it means that check() method must be called only once with "Hello everybody!":
- At this time we need to create a new SpellChecker interface:
- The next step is to provide an spell checker mock to the application context:
- Remove the fail() from the test and run all tests and you will get a test failure saying: 'Wanted but no invoked: spellChecker.check("Hello everybody!")'. That makes sense since we are not invoking the SpellChecker.check() from Editor.paste(). In fact haven't create any kind of dependency Editor -> SpellChecker. Let's do it at class level and force its injection at construction time using Lombok's @RequiredArgsConstructor:
- Declare the constructor injection in the application context as well and re-run the tests:
- Oops! The same failure: 'Wanted but no invoked: spellChecker.check("Hello everybody!")'. Implement the SpellChecker.check() call and run the tests again:
4.0.0 demos.sf.editor spring-hello-word 1.0 Spring Framework Hello World Spring Framework Hello World demo w/ Unit Tests UTF-8 3.2.0.RELEASE junit junit 4.10 org.springframework spring-test ${spring.version} org.springframework spring-beans ${spring.version} org.springframework spring-context ${spring.version} org.mockito mockito-all 1.9.5 org.projectlombok lombok 0.11.6
package demos.sf.editor; import static org.junit.Assert.fail; import org.junit.Test; public class EditorTest { @Test public void testPasteSuccess() { fail(); } }Making the test fail it's very important, if you start failing you won't forget it until its fixed and the test pass. So the "last thing" to do is to remove the failing statement.
... import static org.junit.Assert.assertEquals; public class EditorTest { @Test public void testPasteSuccess() { String expected = "Hello everybody!"; String actual = editor.getText(); assertEquals(expected, actual); fail(); } }The code above will fail to compile, because there isn't and variable/field called editor. This is perfectly normal in TDD, guide your design only by needs (the tests).
public class EditorTest { @Test public void testPasteSuccess() { Editor editor; String expected = "Hello everybody!"; String actual = editor.getText(); assertEquals(expected, actual); fail(); } }The code above, still without compiling due to the non-existing class Editor. Right click on Editor and "Create a new class...".
package demos.sf.editor; import lombok.Getter; public class Editor { @Getter private String text; }NOTE: Lombok must be attached to Spring Tool Suite (or Eclipse) for completion at development time. Copy your lombok.jar to STS installation folder and append the following settings to your STS.ini (eclipse.ini):
-javaagent:/home/lago/Soft/springsource2.9.2/sts-2.9.2.RELEASE/lombok.jar -Xbootclasspath/a:/home/lago/Soft/springsource2.9.2/sts-2.9.2.RELEASE/lombok.jarAlternative open a terminal/command prompt and run:
$ java -jar ~/.m2/repository/org/projectlombok/lombok/0.11.6/lombok-0.11.6.jarAn install wizard gets launched. Choose your STS.ini (eclipse.ini) and press Install/Update. Finally restart your IDE.
public class EditorTest { @Test public void testPasteSuccess() { Editor editor; editor.paste("Hello everybody!"); String expected = "Hello everybody!"; String actual = editor.getText(); assertEquals(expected, actual); fail(); } }
public class Editor { @Getter private String text; public void paste(String cut) { } }Go back to the test, don't waste your time implementing anything at this moment, the test will drive you to that point later.
... import org.springframework.beans.factory.annotation.Autowired; public class EditorTest { @Autowired private Editor editor; @Test public void testPasteSuccess() { editor.paste("Hello everybody!"); String expected = "Hello everybody!"; String actual = editor.getText(); assertEquals(expected, actual); fail(); } }
... import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/test-applicationContext.xml" }) public class EditorTest { ... }
mvn test
... import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/test-applicationContext.xml" }) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) public class EditorTest { ... }
... public class EditorTest { ... @Test public void testAddParagraphSpellIsChecked() { fail(); } }
... import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/test-applicationContext.xml" }) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) public class EditorTest { ... @Autowired private SpellChecker spellChecker; @Test public void testAddParagraphSpellIsChecked() { editor.paste("Hello everybody!"); verify(spellChecker, only()).check("Hello everybody!"); fail(); } }
package demos.sf.editor; public interface SpellChecker { void check(String text); }
package demos.sf.editor; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class Editor { @Getter private String text; private final SpellChecker spellChecker; public void paste(String cut) { if (text == null) { text = ""; } text += cut; } }
package demos.sf.editor; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class Editor { @Getter private String text; private final SpellChecker spellChecker; public void paste(String cut) { if (text == null) { text = ""; } text += cut; spellChecker.check(cut); } }All tests should now pass:
Final Snapshot
- The test case at : src/test/java/demos/sf/editor/EditorTest.java:
package demos.sf.editor; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "/test-applicationContext.xml" }) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) public class EditorTest { @Autowired private Editor editor; @Autowired private SpellChecker spellChecker; @Test public void testPasteSuccess() { editor.paste("Hello everybody!"); String expected = "Hello everybody!"; String actual = editor.getText(); assertEquals(expected, actual); } @Test public void testAddParagraphSpellIsChecked() { editor.paste("Hello everybody!"); verify(spellChecker, only()).check("Hello everybody!"); } }
package demos.sf.editor; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class Editor { @Getter private String text; private final SpellChecker spellChecker; public void paste(String cut) { if (text == null) { text = ""; } text += cut; spellChecker.check(cut); } }
package demos.sf.editor; public interface SpellChecker { void check(String text); }
You can obtain the source code from here. Enjoy it!
I want to say thank you to you. This writing is so useful to me. It saved my life.
ReplyDeleteThanks for your feedback. Also check this improvement of the same example: http://eduardo-lago.blogspot.com/2013/02/becoming-unaware-of-dependency.html
DeleteIt's currently a work in progress. So more examples will be delivered soon!
Regards!
This helps me a lot.. thank you
ReplyDeleteThis comment has been removed by the author.
ReplyDelete