Last time I finished with a failing test case which drove the development to a phase where I had to deal with a sentence instead of a word. The fix was not a big deal, but I ended up with a method in SimpleTextMunger
which did three things:
// ...
public String execute(String sentence) {
StringBuilder mungedSentence = new StringBuilder();
for (String word : sentence.split(" ")) {
mungedSentence.append(mungeAWord(word)).append(" ");
}
return mungedSentence.toString().trim();
}
private String mungeAWord(String word) {
// ... prior content of the execute(String) method
}
-
convert the sentence into words
-
munge a word
-
collect the munged words and create the new sentence
The first and the third one belong together because they work with a sentence, so I created two classes - SentenceHelper
and Munger
-, which are used by the SimpleTextMunger.java
:
public class SimpleTextMunger {
public String execute(String sentence) {
SentenceHelper sentenceHelper = new SentenceHelper();
Munger munger = new Munger();
List words = sentenceHelper.split(sentence);
for (int i = 0; i < words.size(); i++) {
words.set(i, munger.munge(words.get(i)));
}
return sentenceHelper.join(words);
}
}
class Munger {
public String munge(String word) {
// the same as before
}
}
class SentenceHelper {
public List split(String sentence) {
// ...
}
public String join(List words) {
// ...
}
}
One day, I’d like to write unit tests, mocks - you know, the whole package -, so I must move those two new statements out of the scope of the execute method. I’m just going to hack a solution together using spring to see whether it works or not. I can do it now, because all the scenarios are green.
This is the best thing about scenarios and BDD, that I can do experimenting, spiking, refactoring or anything else without changing the test cases. Try to do it with unit tests: the moment you move something, the mocks will turn red and you have to do a lot of unnecessary work by fixing them.
Here is the code - it is ugly -, but it works (if you are new to spring, please have a look at its documentation first):
public String execute(String sentence) {
AbstractApplicationContext factory =
new ClassPathXmlApplicationContext(
new String[] {"/applicationContext.xml"});
SentenceHelper sentenceHelper =
(SentenceHelper) factory.getBean("sentenceHelper");
Munger munger = (Munger) factory.getBean("munger");
List words = sentenceHelper.split(sentence);
for (int i = 0; i < words.size(); i++) {
words.set(i, munger.munge(words.get(i)));
}
return sentenceHelper.join(words);
}
The applicationContext.xml
is very simple: has nothing more than a component-scan
directive:
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zsoltfabok.blog" />
</beans>
It looks better after a couple of small changes, SimpleTextMunger.java
:
@Component
public class SimpleTextMunger {
private final SentenceHelper sentenceHelper;
private final Munger munger;
@Autowired
public SimpleTextMunger(SentenceHelper sentenceHelper,
Munger munger) {
this.sentenceHelper = sentenceHelper;
this.munger = munger;
}
public String execute(String sentence) {
List words = sentenceHelper.split(sentence);
for (int i = 0; i < words.size(); i++) {
words.set(i, munger.munge(words.get(i)));
}
return sentenceHelper.join(words);
}
}
Now the code doesn’t compile, because the SimpleTextMungerStepsdef.java
uses a default constructor, but after the following change, the code compiles, the test cases run and they are green - SimpleTextMungerStepsdef.java
:
@Given("^I have an instance of my class$")
public void I_have_an_instance_of_my_class() {
munger = new SimpleTextMunger(new SentenceHelper(), new Munger());
}
It is really good that the code is cleaner now, on the other hand it has nothing to do with spring: with the change above I got rid of the factory.getBean()
calls, which I have to bring back somehow. Spring has a nice utility called SpringJUnit4ClassRunner
which does all the ApplicationContext
and factory.getBean()
calls in the background. It should be used like in com/zsoltfabok/blog/spring/SimpleTextMungerTest.java
:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
public class SimpleTextMungerTest {
@Autowired
private SimpleTextMunger simpleTextMunger;
@Test
public void shouldMungeASimpleWord() {
assertEquals("wrod", simpleTextMunger.execute("word"));
}
}
This test case runs, and it is green. Somehow I should integrate this structure into my feature - SimpleTextMunger_Test.java
:
@RunWith(Cucumber.class)
@Cucumber.Options(features="classpath:simple_text_munger.feature")
public class SimpleTextMunger_Test {
}
Now I have a small problem with the @RunWith annotation. @RunWith
tells JUnit to run the test cases in a special environment, and unfortunately there can be only one of these environments.
I thought that there had to be a way to solve this issue, so I had a look at the source of the SpringFactory and it looked like the functionality I needed was already there:
public class SpringFactory implements ObjectFactory {
private static AbstractApplicationContext applicationContext;
private StaticApplicationContext stepContext;
private final Collection> stepClasses = new ArrayList>();
static {
applicationContext =
new ClassPathXmlApplicationContext(
new String[]{"cucumber.xml"});
applicationContext.registerShutdownHook();
}
// ...
}
So the only thing I have to do is create a cucumber.xml
and make it visible for the test cases:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="cucumber.runtime.java.spring"/>
<context:annotation-config/>
<import resource="classpath*:/applicationContext.xml"/>
</beans>
The SpringFactory
loads the ClassPathXmlApplicationContext
using the cucumber.xml
- which is an applicationContext.xml
by the way -, and loads every available applicationContext.xml
:
<beans>
<context:component-scan base-package="cucumber.runtime.java.spring" />
<context:annotation-config />
<import resource="classpath*:/applicationContext.xml" />
</beans>
So technically it does all the work I need. Let’s change the step definitions - SimpleTextMungerStepsdef.java
:
public class SimpleTextMungerStepsdef {
@Autowired
private SimpleTextMunger munger;
private String result;
@Given("^I have an instance of my class$")
public void I_have_an_instance_of_my_class() {
}
// ...
}
After adding the @Autowired
annotation and removing the content of the I_have_an_instance_of_my_class
step, my feature executes just fine. Although the step, is empty I’m going to leave it like that for now, because it makes the scenarios readable, and I also may need this step in the future.
So far so good, because cucumber-jvm covers every aspect I need when I’m working with Java code. In the next post I’m going to see how cucumber-jvm works with mocking frameworks. Until thatyou can find the source for this post under the episode_3 branch of the repository on github. Stay tuned!
comments powered by Disqus