junit-team / junit5

✅ The 5th major version of the programmer-friendly testing framework for Java and the JVM
https://junit.org
Other
6.39k stars 1.48k forks source link

Parameterized BeforeEach or AfterEach only #3157

Closed sbernard31 closed 1 year ago

sbernard31 commented 1 year ago

My need :

I have a class containing several tests. I have a @BeforeEach and @AfterEach which setup and tear down each tests.

Now I would like to parameterize test but I don't want to get param on test method but only on @BeforeEach method.

Here is a very simplified example of code of what I would like to do : (In my real use case, my start() method is far more complicated with more than one object to initialize and more than 1 argument as parameter)

public class TestSuite {

    private MyObjectToTest objectToTest;

    @ParameterizedForEach(name = "My custom name {}")
    @ValueSource(strings = { "param1", "param2", "param3"})
    @BeforeEach
    public void start(String param) {
        objectToTest = new MyObjectToTest(param);
    }

    @AfterEach
    public void stop() throws InterruptedException {
        objectToTest.destroy();
    }

    @Test
    public void test1() {
        assertThat(objectToTest).isValid();
    }

    @Test
    public void test2() {
        assertThat(objectToTest).isOK();
    }

My solution for now :

I used the CustomParameterResolver way described at

but this does not reallly elegant as I need to add argument to test method too. This looks like :

@ExtendWith(CustomParameterResolver.class)
public class TestSuite {

    @ParameterizedTest(name = "My custom name {}")
    @ValueSource(strings = { "param1", "param2", "param3"})
    @Retention(RetentionPolicy.RUNTIME)
    private @interface TestAllParams {
    }

    private MyObjectToTest objectToTest;

    @BeforeEach
    public void start(String param) {
        objectToTest = new MyObjectToTest(param);
    }

    @AfterEach
    public void stop() throws InterruptedException {
        objectToTest.destroy();
    }

    @TestAllParams
    public void test1(String param)  {
        assertThat(objectToTest).isValid();
    }

    @TestAllParams
    public void test2(String param)  {
        assertThat(objectToTest).isOK();
    }

Question ?

Do you think this feature request make sense ? Any advice to achieve this in a better / more elegant way ?

marcphilipp commented 1 year ago

It looks like what you're after is class-level parameterization that is tracked in https://github.com/junit-team/junit5/issues/878.

sbernard31 commented 1 year ago

@marcphilipp, :pray: thx for reply.

I tried to read the whole issue. If I well understand class-level parameterization does not exist yet ? right ?

The proposed design would be https://github.com/junit-team/junit5/issues/878#issuecomment-354544841.

So in my case it would looks like :

@ParameterizedContainer(name = ...)
@ValueSource(strings = { "param1", "param2", "param3"})
public class TestSuite {
    @Parameter(0)
    private String param;     

    private MyObjectToTest objectToTest;

    @BeforeEach
    public void start() {
        objectToTest = new MyObjectToTest(param);
    }

    @AfterEach
    public void stop() throws InterruptedException {
        objectToTest.destroy();
    }

    @Test
    public void test1() {
        assertThat(objectToTest).isValid();
    }

    @Test
    public void test2() {
        assertThat(objectToTest).isOK();
    }

It should fit my needs. :+1:

Ideally instead of having parameter as attribute I would prefer to get it as argument like this :sparkles: :

@ParameterizedContainer(name = ...)
@ValueSource(strings = { "param1", "param2", "param3"})
public class TestSuite {

    @BeforeEach
    @Parameterized
    public void start(String param) {
        objectToTest = new MyObjectToTest(param);
    }

    ... ...

OR

@ParameterizedContainer(name = ...)
@ValueSource(strings = { "param1", "param2", "param3"})
public class TestSuite {

    @BeforeEach
    public void start(@Parameter(0) String param) {
        objectToTest = new MyObjectToTest(param);
    }

    ... ...

Sorry for the boring question but it is plan at short/mid term ? Do you know if someone is working on this currently ? Do you think this is something achievable for a new contributor ?

sbernard31 commented 1 year ago

Reading #878, I find other work around, so I decide to list it here.

1) using CustomParameterResolver as explain at https://stackoverflow.com/a/69265907/5088764 (Need to add CustomParameterResolver and MappedParameterContext)

@ExtendWith(CustomParameterResolver.class)
public class TestSuite {

    @ParameterizedTest(name = "My custom name {}")
    @ValueSource(strings = { "param1", "param2", "param3"})
    @Retention(RetentionPolicy.RUNTIME)
    private @interface TestAllParams {
    }

    private MyObjectToTest objectToTest;

    @BeforeEach
    public void start(String param) {
        objectToTest = new MyObjectToTest(param);
    }

    @AfterEach
    public void stop() throws InterruptedException {
        objectToTest.destroy();
    }

    @TestAllParams
    public void test1(String param)  {
        assertThat(objectToTest).isValid();
    }

    @TestAllParams
    public void test2(String param)  {
        assertThat(objectToTest).isOK();
    }

2) using TestFactory as explain at https://github.com/junit-team/junit5/issues/878#issuecomment-568879837 (no to add TestingUtils class)

public class TestSuite {
    @TestFactory
    Stream<DynamicNode> runTests() throws Exception {
        return TestingUtils.parameterizedClassTester("param={0}", Tests.class,
               Stream.of(Arguments.of("param1"), Arguments.of("param2"), Arguments.of("param3")));
    }

    static class Tests {

        private String param;     

        public TestSuite(String param){
            this.param = param;
        }

        private MyObjectToTest objectToTest;

        @BeforeEach
        public void start() {
            objectToTest = new MyObjectToTest(param);
        }

        @AfterEach
        public void stop() throws InterruptedException {
            objectToTest.destroy();
        }

        @Test
        public void test1() {
            assertThat(objectToTest).isValid();
        }

        @Test
        public void test2() {
            assertThat(objectToTest).isOK();
        }

3) using abstract/interface + concrete class as explain at https://github.com/junit-team/junit5/issues/871#issuecomment-1330777589 (no need extra class)

public abstract class TestSuite {

   public TestSuite1 extends TestSuite {
       TestSuite1() {
           super("param1")
       }
   }

   public TestSuite2 extends TestSuite {
       TestSuite2() {
           super("param2")
       }
   }

   public TestSuite3 extends TestSuite {
       TestSuite3() {
           super("param3")
       }
   }

    public TestSuite(String param){
        this.param = param;
    }
    private String param;     

    private MyObjectToTest objectToTest;

    @BeforeEach
    public void start() {
        objectToTest = new MyObjectToTest(param);
    }

    @AfterEach
    public void stop() throws InterruptedException {
        objectToTest.destroy();
    }

    @Test
    public void test1() {
        assertThat(objectToTest).isValid();
    }

    @Test
    public void test2() {
        assertThat(objectToTest).isOK();
    }

All of those workarounds have drawback but it could help waiting for class-level parameterization

stale[bot] commented 1 year ago

If you would like us to be able to process this issue, please provide the requested information. If the information is not provided within the next 3 weeks, we will be unable to proceed and this issue will be closed.

sbernard31 commented 1 year ago

please provide the requested information.

Maybe I totally missed something but I'm not sure what are requested information ?

marcphilipp commented 1 year ago

Maybe I totally missed something but I'm not sure what are requested information ?

Never mind, we should have removed the "waiting-for-feedback" label after your previous comment.

Team decision: We'll take your feedback into consideration in the context of #878.