views:

504

answers:

5

This is an example of a pattern I've encountered a lot recently. I have a method to be tested that takes a List and may invoke some other method(s) for each item in the list. To test this I define an Iterator with the expected call parameters and a loop in the JMock expectations to check the call is made against each item of the iterator (see trivial example below).

I've had a look at the Hamcrest matchers but haven't found something that tests for this (or have misunderstood how the available matchers work). Does anyone have a more elegant approach?

package com.hsbc.maven.versionupdater;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.maven.plugin.testing.AbstractMojoTestCase;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.Sequence;
import org.jmock.internal.NamedSequence;

public class FooTest extends AbstractMojoTestCase {

    public interface Bar {
        void doIt(String arg);
    }

    public class Foo {

        private Bar bar;

        public void executeEven(final List<String> allParameters) {
            for (int i = 0; i < allParameters.size(); i++) {
                if (i % 2 == 0) {
                    bar.doIt(allParameters.get(i));
                }
            }
        }

        public Bar getBar() {
            return bar;
        }

        public void setBar(final Bar bar) {
            this.bar = bar;
        }

    }

    public void testExecuteEven() {
        Mockery mockery = new Mockery();

        final Bar bar = mockery.mock(Bar.class);
        final Sequence sequence = new NamedSequence("sequence");

        final List<String> allParameters = new ArrayList<String>();
        final List<String> expectedParameters = new ArrayList<String>();

        for (int i = 0; i < 3; i++) {
            allParameters.add("param" + i);
            if (i % 2 == 0) {
            expectedParameters.add("param" + i);
            }
        }

        final Iterator<String> iter = expectedParameters.iterator();

        mockery.checking(new Expectations() {
            {
                while (iter.hasNext()) {
                    one(bar).doIt(iter.next());
                    inSequence(sequence);
                }
            }
        });

        Foo subject = new Foo();
        subject.setBar(bar);
        subject.executeEven(allParameters);
        mockery.assertIsSatisfied();
    }
}
+1  A: 

Perhaps the following (using JMockit instead of jMock)?


import java.util.*;

import org.junit.*;
import org.junit.runner.*;

import org.hamcrest.*;
import static org.hamcrest.core.AnyOf.*;
import static org.hamcrest.core.Is.*;
import org.hamcrest.core.*;

import mockit.*;
import mockit.integration.junit4.*;

@RunWith(JMockit.class)
public class FooTest
{
   public interface Bar { void doIt(String arg); }

   public class Foo
   {
      private Bar bar;

      public void executeEven(final List<String> allParameters)
      {
         for (int i = 0; i < allParameters.size(); i++) {
            if (i % 2 == 0) {
               bar.doIt(allParameters.get(i));
            }
         }
      }

      public void setBar(final Bar bar) { this.bar = bar; }
   }

   @Test
   public void testExecuteEven(final Bar bar)
   {
      final List<String> allParameters = new ArrayList<String>();
      final List<Matcher<? extends String>> expectedParameters =
         new ArrayList<Matcher<? extends String>>();

      for (int i = 0; i < 3; i++) {
         allParameters.add("param" + i);

         if (i % 2 == 0) {
            expectedParameters.add(new IsEqual<String>("param" + i));
         }
      }

      new Expectations()
      {
         {
            bar.doIt(with(anyOf(expectedParameters))); repeats(expectedParameters.size());
         }
      };

      Foo subject = new Foo();
      subject.setBar(bar);
      subject.executeEven(allParameters);
   }

   @Test // a shorter version of the same test
   public void testExecuteEven2(final Bar bar)
   {
      final List<String> allParameters = Arrays.asList("param0", "param1", "param2");

      new Expectations()
      {
         {
            bar.doIt(with(anyOf(is("param0"), is("param2")))); repeats(2);
         }
      };

      Foo subject = new Foo();
      subject.setBar(bar);
      subject.executeEven(allParameters);
   }
}
Rogerio
thanks, it's certainly the kind of thing I had in mind and will use it in future but I have limited choice for the test frameworks I can use on this project.
Rich Seller
This version doesn't actually test the same thing. It asserts only that there are `expectedParameters.length()` calls to `bar.doIt()` and that each call will pass one of the elements of `expectedParameters`. It will not verify that each element is passed exactly once.If your code contained a bug in that it failed to iterate over the list, this test would not find it.
Kris Pruden
+1  A: 

I think your current test implementation is pretty close to ideal. Any further compaction risks either changing the semantics of the test or obscuring the intent of the test to a reader (or both).

However, if you're looking for a way to expect a specific number of calls to a method, you can use exactly(n).of():

mockery.checking(new Expectations() {{
  exactly(expectedParameters.length()).of(bar).doIt(with(anyOf(expectedParameters)));
}});

(I left out the evenness check, but you get the idea). This is similar to the jmockit example in a different answer. Be aware that this does not test the same thing as your original test. In particular it does not check:

  1. The order of the calls to doIt
  2. That each element of the parameter list is passed exactly once

For example, this test would pass if your method iterated over the list in reverse order, or if it just called the doIt method n times but passed the first element of the list each time. If you want to ensure that each element in the list is passed, you pretty much have to iterate over it setting an individual expectation for each. If you don't care about the order of the invocations, you can omit the use of the Sequence (in that case you may want to change your original method to accept a Collection instead of a List).

Kris Pruden
A: 

You can simplify this test. You know what you want, so you can be more concrete about the code:

public void testExecuteEven() {
  final List<String> values = Arrays.asList("param0", "param1", "param2", "param3");
  Sequence evens = mockery.sequence("evens");

  mockery.checking(new Expectations() {{
    oneOf(bar).doIt(values.get(0)); inSequence(evens);
    oneOf(bar).doIt(values.get(2)); inSequence(evens);
  }});

  subject.executeEven(values);
}

If you're using JUnit 4, don't forget that the @RunWith(JMock.class) annotation on the class avoids the need for the assertIsSatisfied() call.

Steve Freeman
A: 

b.t.w. posting to the jmock list might be a better venue for this kind of question.

Steve Freeman
A: 

It's worth remembering that you don't have to create your expectations all at once. You can do your loop outside of the checking(new Expectations(){{}}) block and manipulate the expectations list before finally passing it in to the mockery. This can help with clarity in complicated expectation setups (and so does commenting!):

@Test
public void testExecuteEven() {

  Mockery mockery = new Mockery();
  Sequence evens = mockery.sequence("evens");
  final Bar bar = mockery.mock(Bar.class);

  List<Expectations> expectations = new ArrayList<Expectations>();

  final List<String> allParameters = new ArrayList<String>();
  final List<String> expectedParameters = new ArrayList<String>();


  // generate some parameters 
  for (int i = 0; i < 3; i++) {
      allParameters.add("param" + i);
      if (i % 2 == 0) {
      expectedParameters.add("param" + i);
      }
  }

  // define expectations for the expected parameters
  for (String param : expectedParameters) {
    expectations.add(new Expectations() {{ oneOf(bar).doIt(param); inSequence(evens); }});
  }

  // define any special expectations here
  expectations.add(new Expectations() {{ oneOf(bar).doSomethingElse() /* whatever */ }});

  // load the expectations into the mockery
  for (Expectations expectation : expectations) {
    mockery.checking(expectation);
  }

  Foo subject = new Foo();
  subject.setBar(bar);
  subject.executeEven(allParameters);

}

Also, I notice you're not using Java 5 foreach statements. If you're not stuck using Java 4 that can also help with clarity.

joshwa