Checkwhei Sin
Checkwhei Sin

Reputation: 185

How to test different implementations for an interface in Junit5 without duplicating the code

May I ask how to write a junit 5 test for an interface with different implementations?

For example, I have a interface Solution, with different implementations like SolutionI, SolutionII, can I write only one test class to test both?

There is a post shows an example, but if there are multiple test method that needs to be called, I have to pass the parameter for every test method.

May I ask if there is an elegant way like what is used in the Junit4

In Junit4, I have a very elegant code sample as follows

@RunWith(Parameterized.class)
public class SolutionTest {
  private Solution solution;

  public SolutionTest(Solution solution) {
    this.solution = solution;
  }

  @Parameterized.Parameters
  public static Collection<Object[]> getParameters() {
    return Arrays.asList(new Object[][]{
        {new SolutionI()},
        {new SolutionII()}
    });
  }
  // normal test methods
  @Test
  public void testMethod1() {

  }
}

Junit 5 claims ExtendWith() is similar, I tried the following code

@ExtendWith(SolutionTest.SolutionProvider.class)
public class SolutionTest {
  private Solution solution;

  public SolutionTest(Solution solution) {
    System.out.println("Call constructor");
    this.solution = solution;
  }

  @Test
  public void testOnlineCase1() {
    assertEquals(19, solution.testMethod(10));
  }

  @Test
  public void testOnlineCase2() {
    assertEquals(118, solution.testMethod(100));
  }

  static class SolutionProvider implements ParameterResolver {
    private final Solution[] solutions = {
        new SolutionI(),
        new SolutionII()
    };
    private static int i = 0;

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
      return parameterContext.getParameter().getType() == Solution.class;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
      System.out.println(i);
      return solutions[i++];
    }
  }
}

Unfortunately, testMethod1 is using SolutionI and testMethod2 is using SolutionII, which makes sense, I don't know if this helps to inspire a little bit.

Thanks for the help in advance

Upvotes: 7

Views: 4196

Answers (3)

Daniils Loptevs
Daniils Loptevs

Reputation: 11

I tried lotor but for me it doesn't work from start and I found 2 ways to make it works.

jupiter : 5.9.2 (Jan 10, 2023) https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api

1 - Declare Implements Classes inside Test Interface. Code example:

public interface StringDiagnoseTest<T extends StringDiagnose> {
    
    T createDiagnose();
    
    @Test
    default void blankCheckFollowsContract() {
        assertTrue(createDiagnose().isTheStringBlank("\t\n "));
        assertFalse(createDiagnose().isTheStringBlank("\t\n !  \r\n"));
    }
    
    class DefaultDiagnoseTest implements StringDiagnoseTest<DefaultDiagnose> {
    
        @Override
        public DefaultDiagnose createDiagnose() {
            return new DefaultDiagnose();
        }
    }

    class StreamBasedDiagnoseTest implements StringDiagnoseTest<StreamBasedDiagnose> {
    
        @Override
        public StreamBasedDiagnose createDiagnose() {
            return new StreamBasedDiagnose();
        }
    }
}

2 - If you use Intelligent IDEA, check the build/run configuration, it should looks like that:

For property "The type of resource to search for tests" select pattern and value:

my_test_package.StringDiagnoseTest$DefaultDiagnoseTest||
my_test_package.StringDiagnoseTest$StreamBasedDiagnoseTest

If you have problem, probably(by default) selected class and value StringDiagnoseTest.

Upvotes: 1

Checkwhei Sin
Checkwhei Sin

Reputation: 185

Sorry for not replying to this thread for a while. Comparing to the lotor's answer, I found some other ways I am currently adopting:


  @ParameterizedTest
  @MethodSource("solutionStream")
  void testCase(Solution solution) {
   // add your test
  }

  static Stream<Solution> solutionStream() {
    return Stream.of(
        new SolutionI(),
        new SolutionII()
    );
  }

The constructor needs parameters (Not type-safe)

  @ParameterizedTest
  @MethodSource("solutionStream")
  void testOnlineCase(Class<Solution> solutionClass) throws NoSuchMethodException, IllegalAccessException,
      InvocationTargetException, InstantiationException {
    Solution solution = solutionClass.getConstructor(Integer.TYPE).newInstance(2);
  }

  static Stream<Class> solutionStream() {
    return Stream.of(
        SolutionI.class
    );
  }

Upvotes: 1

lotor
lotor

Reputation: 1090

Jupiter provides Test interfaces exactly for your purpose - to test interface contract.

For example, let's have an interface for string diagnostic contract and two implementations following the contract but exploiting different implementation ideas:

public interface StringDiagnose {
    /**
     * Contract: a string is blank iff it consists of whitespace chars only
     * */
    boolean isTheStringBlank(String string);
}

public class DefaultDiagnose implements StringDiagnose {

    @Override
    public boolean isTheStringBlank(String string) {
        return string.trim().length() == 0;
    }
}

public class StreamBasedDiagnose implements StringDiagnose {

    @Override
    public boolean isTheStringBlank(String string) {
        return string.chars().allMatch(Character::isWhitespace);
    }
}

According to the recommended approach you are to create test interface that verifies the diagnostic contract in default methods and exposes implementation-dependent pieces to hooks:

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

public interface StringDiagnoseTest<T extends StringDiagnose> {

    T createDiagnose();

    @Test
    default void blankCheckFollowsContract(){
        assertTrue(createDiagnose().isTheStringBlank("\t\n "));
        assertFalse(createDiagnose().isTheStringBlank("\t\n !  \r\n"));
    }
}

and then implement this test interface for each solution specific:

class DefaultDiagnoseTest implements StringDiagnoseTest<DefaultDiagnose> {

    @Override
    public DefaultDiagnose createDiagnose() {
        return new DefaultDiagnose();
    }
}

class StreamBasedDiagnoseTest implements StringDiagnoseTest<StreamBasedDiagnose> {

    @Override
    public StreamBasedDiagnose createDiagnose() {
        return new StreamBasedDiagnose();
    }
}

Use more hooks and not-default interface methods to test same-named solutions' aspects (like performance) and define new tests in the interface implementations for completely distinctive implementation pecularities.

Upvotes: 11

Related Questions