Java Testing with Spectrum and Spring

If you’re a Java developer and you haven’t tried out Spectrum yet, I encourage you to give it a try. It’s a BDD-style testing framework that resembles Rspec, Jasmine, and Mocha — popular frameworks for Ruby and Javascript testing. Similarly, if you haven’t tried Spring or haven’t tried it in the last few years, take a look at Spring Boot. Spring has come a long way from its days of endless xml markup. That said, putting the two frameworks together can be a bit tricky.

In this post I’ll show you how to use Spectrum with Spring and talk about some of the trade-offs we made in setting up our tests. The Spring/Spectrum tests have a fair amount of boilerplate, but for my team, the organizational power of describe() blocks is worth it.

I’m a long time TDD-er. A few years and a couple jobs ago I had a begrudging acceptance of Junit tests that I wrote about here. Nonetheless I was still on the lookout for more Mocha-like Java testing framework. When Java 8 came out with Lambdas and Streams, I knew my dream framework must be right around the corner. I was so excited when I stumbled upon Spectrum.

For comparison, here is a Mocha test:

describe("CalendarUpdater", () => {  let eventRepository, calendarUpdater;  beforeEach(() => {      eventRepository = new EventRepository();      eventRepository.deleteAll();      calendarUpdater = new CalendarUpdater(eventRepository);  });    describe("#updateCalendar", () => {    describe("when there is a new event in the calendar", () => {      let calendar;      beforeEach(() => {        calendar = new Calendar({ events: new Event("Birthday", new Date())});      });            it("saves the event", () => {        calendarUpdater.updateCalendar(calendar);        expect(eventRepository.findAll()).to.haveLength(1);      });    });  });});

And here is a Spectrum test:

@RunWith(Spectrum.class)public class CalendarUpdaterTest {  EventRepository eventRepository;  Calendar calendar;  CalendarUpdater calendarUpdater;  {    beforeEach(() -> {      eventRepository = new EventRepository();      eventRepository.deleteAll();      calendarUpdater = new CalendarUpdater(eventRepository);    });        describe("#updateCalendar", () -> {      describe("when there is a new event in the calendar", () -> {        beforeEach(() -> {          calendar = new Calendar(List.of(new Event("Birthday", new Date())));        });         it("saves the event", () -> {            calendarUpdater.updateCalendar(calendar);            assertThat(eventRepository.findAll()).hasSize(1);        });      });    });  }}

Aside from a little syntactic sugar, the tests are almost identical!

Note: Any variables that you want to set in a beforeEach() need to be declared at the class level, or you’ll get a compilation error that variables used in lambda expression must be final or effectively final. This is because a lambda in Java works as though it were calling a method on an anonymous class defined by the lambda declaration. The variables declared in the outer scope that are used inside the lambda are copied to fields on the anonymous class. If assignment were allowed, the value would be set on the lambda’s anonymous class instance, rather than assigning to the variable declared in the outer scope. That would be more confusing than useful, so instead all the anonymous class’s fields are declared as final. This is different from how scope sharing works in Javascript, which is why variables assigned in a Mocha beforeEach can be declared inside of the describe.

Also note: That set of curly braces around the beforeEach() and describe()defines a constructor for CalendarUpdaterTestNeat!

In general, Spectrum worked great out-of-the-box for unit tests where we instantiated mock dependencies using Mockito (e.g. eventRepository = mock(EventRepository.class);) and for tests where real instances of dependencies were easy to create (e.g. eventRepository = new EventRepository()). However, in some tests we wanted to take advantage of the Spring framework to inject real instances of our dependencies that were hard to set up by hand. For example, we wanted to ensure that the custom database queries on our Hibernate repositories retrieved the expected data.

For Junit tests, there is a custom runner that spins up a Spring context:

@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = {MyApplication.class})public class CalendarUpdaterTest {  @Autowired  EventRepository eventRepository;    CalendarUpdater calendarUpdater;    @Before  public void setUp(){    eventRepository.deleteAll();    calendarUpdater = new CalendarUpdater(eventRepository);  }    @Test  public void updateCalendar_whenThereIsAnEvent_savesTheEvent() {    Calendar calendar = new Calendar(List.of(new Event("Birthday", new Date())));    calendarUpdater.updateCalendar(calendar);    assertThat(eventRepository.findAll()).hasSize(1);  }}

We looked around the web for a SpringSpectrumRunner but came up empty-handed. For a little while we were writing our unit tests in Spectrum, but for tests that used Spring, we were using plain old Junit. Do what works! We really liked the Spectrum tests better though, so we revisted the idea of writing all of our tests using Spectrum. We realized we could explicitly create a Spring context to inject the dependencies we wanted.

For tests where we use Spring, this is our current setup:

@RunWith(Spectrum.class)public class CalendarUpdaterTest {  @Component  private static class TestData {    Calendar calendar;    CalendarUpdater calendarUpdater;        @Autowired    EventRepository eventRepository;  }    private static class SpringTestContext {    private AutowireCapableBeanFactory beanFactory;    private TestData data;      void initialize() {      ApplicationContext context = new AnnotationConfigApplicationContext("com.example.yourpackagename");      this.beanFactory = context.getAutowireCapableBeanFactory();      this.data = (TestData) beanFactory.autowire(TestData.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);    }  }    private SpringTestContext context = new SpringTestContext();    private TestData test() {    return this.context.data;  }    {    context.initialize();    beforeEach(() -> {      test().eventRepository = new EventRepository();      test().eventRepository.deleteAll();      test().calendarUpdater = new CalendarUpdater(eventRepository);    });        describe("#updateCalendar", () -> {      describe("when there is a new event in the calendar", () -> {        beforeEach(() -> {          test().calendar = new Calendar(List.of(new Event("Birthday", new Date())));        });                    it("saves the event", () -> {          test().calendarUpdater.updateCalendar(calendar);          assertThat(test().eventRepository.findAll()).hasSize(1);        });      });    });  }}

Obviously, this is a lot more boilerplate than the Junit version. We’re explicitly creating an ApplicationContext and declaring an inner TestData class all just so that we can autowire eventRepository. However, as we create more tests for CalendarUpdater, that boilerplate won’t really grow. We’ll add some more fields to TestData, but that’s it. Meanwhile, the contents of our Spectrum test methods will be smaller, clearer, and better organized than their Junit counterparts. Our Spectrum tests can share setup for each scenario we describe(), allowing the it()s to contain only the call to the target action and the assertions about the result of the action. That makes the tests easier to read and change. We write lots of tests, so for us, it’s worth it!

One thing you may need to watch out for when spinning up Spring contexts explicitly like this is that you may use up limited resources like database connections. You can sidestep the issue in a variety of ways. For example, you could ensure that you’re properly freeing resources at the end of each test, or you could share a resource pool instance across tests. I’ll leave that for a future blog post though.

Do you have another way of running Spectrum with Spring? Let us know in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *