Using non-static injected services in JUnit Parameterized Tests

5.5k views Asked by At

I want to use Guice and GuiceBerry to inject a non-static legacy service into a factory class. I then want to inject that factory into my Parameterized JUnit test.

However, the issue is JUnit requires that the @Parameters method be static.

Example factory:

@Singleton
public class Ratings {
    @Inject
    private RatingService ratingService;

    public Rating classicRating() {
         return ratingService.getRatingById(1002)
    }

    // More rating factory methods
}

Example test usage:

@RunWith(Parameterized.class)
public class StaticInjectParamsTest {
    @Rule
    public GuiceBerryRule guiceBerryRule = new GuiceBerryRule(ExtendedTestMod.class)

    @Inject
    private static Ratings ratings;

    @Parameter
    public Rating rating;

    @Parameters
    public static Collection<Rating[]> ratingsParameters() {
    return Arrays.asList(new Rating[][]{
            {ratings.classicRating()}
            // All the other ratings
        });
    }

    @Test
    public void shouldWork() {
        //Use the rating in a test

    }
}

I've tried requesting static injection for the factory method but the Parameters method gets called before the GuiceBerry @Rule. I've also considered using just the rating's Id as the parameters but I want to find a reusable solution. Maybe my approach is flawed?

4

There are 4 answers

3
Andrew Bocz Jr On BEST ANSWER

My solution was to add a RatingId class that wraps an integer and create a factory RatingIds that I could then return static and use as parameters. I overloaded the getRatingById method in my RatingService interface to accept the new RatingId type, and then inject the rating service into my test and use it directly.

Added factory:

public class RatingIds {
    public static RatingId classic() {
        return new RatingId(1002);
    }
    // Many more
}

Test:

@RunWith(Parameterized.class)
public class StaticInjectParamsTest {
    @Rule
    public GuiceBerryRule guiceBerryRule = new GuiceBerryRule(ExtendedTestMod.class)

    @Inject
    private RatingService ratingService

    @Parameter
    public RatingId ratingId;

    @Parameters
    public static Collection<RatingId[]> ratingsParameters() {
    return Arrays.asList(new RatingId[][]{
        {RatingIds.classic()}
        // All the other ratings
        });
    }

    @Test
    public void shouldWork() {
        Rating rating = ratingService.getRatingById(ratingId.getValue())
        //Use the rating in a test

    }
}
2
Jan Galinski On

I did not get guiceberry to run (ancient dependencies), but using JUnitParamters and plain guice, this is rather simple:

@RunWith(JUnitParamsRunner.class)
public class GuiceJunitParamsTest {

    public static class SquareService {
        public int calculate(int num) {
            return num * num;
        }
    }

    @Inject
    private SquareService squareService;

    @Before
    public void setUp() {
        Guice.createInjector().injectMembers(this);
    }

    @Test
    @Parameters({ "1,1", "2,4", "5,25" })
    public void calculateSquares(int num, int result) throws Exception {
        assertThat(squareService.calculate(num), is(result));
    }
}

If you check the JUnitParams website, you will find a lot of other ways to define the parameters list. It is really easy to do this with the injecte service.

0
NamshubWriter On

Unfortunately, JUnit needs to be able to enumerate all of the tests before running any tests, so the parameters method must be called before rules.

You could define an enum for the type of rating:

@RunWith(Parameterized.class)
public class StaticInjectParamsTest {
  @Rule
  public GuiceBerryRule guiceBerryRule
      = new GuiceBerryRule(ExtendedTestMod.class);

  @Inject
  private Ratings ratings;

  @Parameter
  public RatingType ratingType;

  @Parameters
  public static Collection<RatingType> types() {
    return Arrays.asList(RatingType.values());
  }

  @Test
  public void shouldWork() {
    Rating rating = ratings.get(ratingType);
    // Use the rating in a test
  }
}

Edit: Code for enum:

public enum RatingType {
  CLASSIC(1002),
  COMPLEX(1020);

  private final int ratingId;

  private RatingType(int ratingId) {
    this.ratingId = ratingId;
  }

  // option 1: keep rating ID private by having a method like this
  public get(RatingService ratingService) {
    return ratingService.getRatingById(ratingId);
  }

  // option 2: have a package-scope accessor
  int getRatingId() {
    return ratingId;
  }
}

Edit: if you go with option 2 you would then add a new method to get a Rating from a RatingType which would delegate to the service passing ratingId:

@Singleton
public class Ratings {
    @Inject
    private RatingService ratingService;

    public Rating getRating(RatingType ratingType) {
      return ratingService.getRatingById(
          ratingType.getRatingId());
    }

    // More rating factory methods
}

If you don't want RatingType to be in your public API, you can define it in your test, and have a method in the enum named getRating()

public enum RatingType {
  CLASSIC {
    @Override public Rating getRating(Ratings ratings) {
      return ratings.getClassicRating();
    }
  },
  COMPLEX {
    @Override public Rating getRating(Ratings ratings) {
      return ratings.getComplexRating();
    }
  };

  public abstract Rating getRating(Ratings ratings);
}

You could also create a value type instead of an enum.

This assumes you can write tests that should pass for all Rating instances.

If you have some common tests but some rating-specific tests, I would make an abstract base class that contains common tests, and an abstract createRating() method, and subclass it for every rating type.

0
Marti Nito On

In cases as yours, where the total number of generated parameter sets is known in advance, but building the parameters itself requires some context (e.g. autowired service instance with Spring) you can go the functional approach (with junit5 & parameterized)

Obviously that does not work, if the createParameter function itself depends on such contex:-/

class MyTestClass {

    // may be autowired, cannot be static but is required in parameter generation
    SomeInstance instance;

    private interface SomeParamBuilder { SomeParam build(SomeInstance i);}

    private static Stream<Arguments> createParamterFactories() {
         return Stream.of(
            Arguments.of((SomeParamBuilder)(i)->     
                            {
                                return new SomeParam(i);
                            })
                         );
    }

    // does not work, because SomeParam needs SomeInstance for construction
    // which is not available in static context of createParameters.
    //@ParameterizedTest(name = "[{index}] {0}")
    //@MethodSource("createParameters")
    //void myTest(SomeParam param) {
    //}


    @ParameterizedTest(name = "[{index}] {0}")
    @MethodSource("createParamterFactories")
    void myTest(SomeParamBuilder builder) {
        SomeParam param = builder.build(instance);
        // rest of your test code can use param.
    }
}

maven dep:

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>5.2.0</version>
            <scope>test</scope>
        </dependency>