8

I've tried to avoid duplicate code in JUnit test, but I'm kind of stuck.

This is my first test, for the second one it has exactly the same methods but different service (different input). instead of the TestCaseResourceTest1 I have TestCaseResourceTest2. Now what could be the proper way to test both? I want to have a separate file for test number 2, how should I avoid the duplicate code? (ex. use the beforeFileTest() method)

public class TestCaseResourceTest1 {

    @Mock
    private TestService testService;
    @Mock
    private AreaService areaService;

    private TestCaseService1 testCaseService1; // is changed in test2

    @Before
    public void before() throws Exception{
        testCaseService1 = mock(TestCaseService1.class); // is changed in test2
        MockitoAnnotations.initMocks(this);
        beforeFileTest();
    }

    private void beforeFileTest() throws Exception{
        doReturn(true).when(areaService).chechExists(any(String.class), eq(false));
    }

    @Test
    public void verifyFileExists() throws Exception{
        verifyOtherArea(testCaseService1); // is changed in test2
        doReturn(false).when(areaService).chechExists(any(String.class), eq(false));
    }
}

just lines with comment is changed in test2 are differences.

Tnx

GeoCom
  • 1,290
  • 2
  • 12
  • 33
  • 3
    Duplicate code is not always bad. Especially in tests where it's better to avoid any extra logic or abstractions. – Lukasz Wiktor Aug 07 '17 at 14:30
  • @LukaszWiktor I wanna try not to have a duplicate code. especially after reading this https://stackoverflow.com/questions/129693/is-duplicated-code-more-tolerable-in-unit-tests – GeoCom Aug 07 '17 at 14:34
  • @Milban what are you testing? As the change, you are talking about it's a service I don't know if avoiding duplication is the best way. – Olimpiu POP Aug 07 '17 at 14:37
  • @LukaszWiktor Really depends on the situation; if you have many tests that share setup code then abstracting it away is very helpful. Some unit tests have a fair amount of setup. – Dave Newton Aug 07 '17 at 14:49
  • 1
    @LukaszWiktor It is not at all true that it's better to avoid extra logic and abstractions in your test code. What's important is avoiding the *wrong* abstractions, which is just as important in your actual code, and duplication is preferable to the wrong abstraction for either of them. The thing is that it's a lot easier to create wrong abstractions in tests if you're just looking at raw code duplication without context, so it's generally better to go with code duplication if your test abstraction skills need work. On the other hand, you have to do it wrong to learn to do it right. – TiggerToo Jul 09 '19 at 12:57

5 Answers5

4

Given this excerpt from your question:

… instead of the TestCaseResourceTest1 I have TestCaseResourceTest2 … I want to have a separate file for test number 2

… the standard ways of sharing code between test cases are:

  • Create a Test Suite and include the shared code in the test suite (typically in @BeforeClass and @AfterClass methods). This allows you to (1) run setup code once (per suite invocation); (2) encapsulate shared setup/teardown code and (3) easily add more tests cases later. For example:

    @RunWith(Suite.class)
    @Suite.SuiteClasses({
        TestCaseResourceTest1.class,
        TestCaseResourceTest2.class
    )}
    public class TestSuiteClass {
    
        @BeforeClass
        public void setup() {
            beforeFileTest();
        }
    
        private void beforeFileTest() throws Exception {
            // ...
        }
    }
    
  • Create an abstract class which parents TestCaseResourceTest1 and TestCaseResourceTest2 and let those test cases call the shared code in the parent (typically via super() calls). With this approach you can declare default shared code in the parent while still allowing sub classes to (1) have their own behaviour and (2) selectively override the parent/default behaviour

  • Create a custom JUnit runner, define the shared behaviour in this runner and then annotate the relevant test cases with @RunWith(YourCustomRunner.class). More details on this approach here

Just to reiterate what some of the other posters have said; this is not a common first step so you may prefer to start simple and only move to suites or abstract classes or custom runners if your usage provides a compelling reason to do so.

glytching
  • 44,936
  • 9
  • 114
  • 120
3

Assuming you want to have the exact same test run for 2 different classes (and not mocking it as in your example code), you can create an abstract test class, that has abstract method that returns an instance of the class to be tested.

Something in the vein of:

public abstract class TestCaseResourceTest {

  protected abstract TestCaseService1 getServiceToTest();

  @Before
  public void before() throws Exception {
    testCaseService1 = getServiceToTest();
    MockitoAnnotations.initMocks(this);
    beforeFileTest();
  }

  @Test
  public void test() {
    // do your test here
  }
}

public class ConcreteTest extends TestCaseResourceTest {
  protected TestCaseService1 getServiceToTest() {
    return new TestCaseService();
  }
}

public class ConcreteTest2 extends TestCaseResourceTest {
  protected TestCaseService1 getServiceToTest() {
    return new DifferentService();
  }
}
André Onuki
  • 428
  • 2
  • 12
3

I had the such situation and it was a sign about wrong implementation design. We are talking about pure unit tests where we test exactly what is implemented in the production classes. If we need duplicated tests it means we probably have duplication in implementation.

How did I resolve it in my project?

  1. Extracted common logic into parent service class and implemented unit tests for it.
  2. For child services I implemented tests only for particular implemented code there. No more.
  3. Implemented an integration tests on real environment were both services were involved and tested completely.
Dmytro Maslenko
  • 2,247
  • 9
  • 16
2

Have you considered using JUnit 5 with its http://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests ?

It allows you to re-use your tests with different input. This is an example from the documentation which illustrates what you can do now with JUnit 5:

@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void testWithStringParameter(String argument) {
    assertNotNull(argument);
}

But you can also create your methods which return the input data:

@ParameterizedTest
@MethodSource("stringProvider")
void testWithSimpleMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> stringProvider() {
    return Stream.of("foo", "bar");
}

Here I am using just strings, but you can really use any objects.

If you are using Maven, you can add these dependencies to start using JUnit 5:

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

The only annoying thing about JUnit 5 is that it is not released yet.

gil.fernandes
  • 12,978
  • 5
  • 63
  • 76
  • This is available in JUnit 4 also. Use `@RunWith(org.junit.runners.Parameterized.class)` and you can do field injection with `@org.junit.runners.Parameterized.Parameter` and test descriptions with `org.junit.runners.Parameterized.Parameters`. There's way too much going on to fit in a comment, but it is there. JUnit 5 makes it nicer, to be sure, but it's not brand-new. – dcsohl Aug 15 '17 at 14:41
0

When going from one test to two tests, you don't know what will be duplicate code, so I find it useful to put everything into one test method. In this case, start by putting the contents of the @Before and beforeFileTest methods inline in the test.

Then you can see that it is just te service that needs changing, so you can extract everything except that into a helper method that is called from two tests.

Also, after you have two tests that are calling the same helper method and are happy with that test coverage, you could look into writing parameterized tests. For example with JunitParams: https://github.com/Pragmatists/junitparams/wiki/Quickstart

Scott Newson
  • 2,745
  • 2
  • 24
  • 26