Cucumber JVM: Dependency Injection

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
}
  1. convert the sentence into words

  2. munge a word

  3. 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