Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract classes with @Nested are not ignored / not handled properly #2717

Closed
Marcono1234 opened this issue Sep 13, 2021 · 5 comments
Closed

Comments

@Marcono1234
Copy link
Contributor

Description

The predicate for whether a class annotated with @Nested can be executed does not check whether the class is abstract, see IsNestedTestClass. This causes a java.lang.InstantiationException when trying to run the tests (from within Eclipse IDE).

The predicate for top level test classes in IsPotentialTestContainer excludes abstract classes.

In case you decide to indeed ignore abstract nested classes, then error handling for that would probably have to be part of #242.

Steps to reproduce

Try to run the following test:

package test;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

public class NestedTestClass {
    @Nested
    public abstract class NestedClass {
        @Test
        public void test() {
            System.out.println("test");
        }
    }
}

Context

  • Used versions (Jupiter/Vintage/Platform): 5.8.0
  • Build Tool/IDE: Eclipse IDE 2021-06 (4.20.0)
@sbrannen
Copy link
Member

What is the use case for having an abstract @Nested test class?

@Nested is not @Inherited, so it cannot apply to a subclass of an abstract @Nested test class.

@sbrannen
Copy link
Member

What is the use case for having an abstract @Nested test class?

Or are you claiming there is no use case and that you would prefer a better (more informative) error message?

@Marcono1234
Copy link
Contributor Author

Or are you claiming there is no use case and that you would prefer a better (more informative) error message?

Yes that was what I meant. Either a better error message or such classes being ignored (similar to other cases mentioned in #242). Personally I would prefer an error to make the user aware of the issue, since it is not known yet when 242 will be resolved.

@marcphilipp
Copy link
Member

Team decision: Let's introduce a proper error message for this scenario when tackling #242.

@thecooldrop
Copy link

Hi @marcphilipp I know that this is closed under the pretense that there is no valid use case for the @nested abstract classes, but I would like to argue otherwise, as I am facing this very limitation within the framework right now.

I am a developing an application where my domain logic is decoupled from outside things ( usually databases but also some other things ) via interfaces. This is where a use case for @Nested within abstract test classes pops up. I am trying to develop the abstract base class, which is going to contain the tests, which must succeed for all implementations of interfaces on which my domain logic relies. Assume that there is an interface called RecipeRepository ( please ignore the concrete details, I am just transcribing from my learning project, and have no time to anonymize the class names), then I would like to develop abstract test class, such that all implementations of the RecipeRepository must pass these tests. Of course I would have a single implementation of this abstract test class for each implementation of the RecipeRepository. Currently my abstract test class looks like following ( I have elided the content of the test methods, as this is boring detail, and you have an example which errors out already ):

package udarnicka.recipes.crud.persistence;

import jdk.jfr.Description;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.data.repository.CrudRepository;
import udarnicka.common.CanonicalName;
import udarnicka.common.HasCanonicalName;
import udarnicka.common.HasId;
import udarnicka.common.SerialInteger;
import udarnicka.recipes.crud.domain.ports.*;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import java.util.*;

import static org.assertj.core.api.Assertions.*;

public abstract class RecipeRepositoryAbstractTest {

    @Nested
    @Description("If there is a database")
    public class GivenADatabase {

        @Nested
        @Description("which contains no recipes")
        public class WhichIsEmpty<Entity extends HasId<?> & HasCanonicalName, DataSource extends CrudRepository<Entity, ?>> {

            protected RecipeRepository recipeRepository;
            protected DataSource dataSource;

            @Test
            @Description("then new Recipes can be saved into the database")
            protected void thenNewRecipesCanBeSavedIntoTheDatabase() {...}

            @Test
            @Description("then recipe with same canonical name can not be saved into database twice")
            protected void recipeWithSameNameCanNotBeCreatedTwice() {...}

            @Test
            @Description("then deleting any recipe returns an empty Optional")
            protected void deletingAnyRecipeReturnsEmtpyOptional() {...}

            @Test
            @Description("then reading any ingredient returns an empty Optional")
            protected void readingANonexistentRecipeReturnsAnEmptyOptional() {...}
        }

        @Nested
        @Description("which contains some recipes")
        public class WhichContainsSomeRecipes<Entity extends HasId<?> & HasCanonicalName, DataSource extends CrudRepository<Entity, ?>> {

            @PersistenceContext
            private EntityManager em;

            private List<Recipe> recipesInDb;

            protected DataSource dataSource;
            protected RecipeRepository recipeRepository;

            protected void initializeData() {...}

            @Test
            @Description("then deleting a recipe removes it from the database")
            protected void deletingTheRecipeRemovesItFromTheDatabase() {...}

            @Test
            @Description("then deleting a recipe returns the recipe")
            protected void deletingTheRecipeReturnsDeletedRecipe() {...}

            @Test
            @Description("then the existing recipe can be read")
            protected void existingRecipeCanBeRead() {...}
        }
    }
}

I would then like to implement this for concrete implementation of RecipeRepository as in:

package udarnicka.recipes.crud.persistence.jpa;

import jdk.jfr.Description;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import udarnicka.recipes.crud.persistence.RecipeRepositoryAbstractTest;

public class JpaRecipeRepositoryTest extends RecipeRepositoryAbstractTest {

    @DataJpaTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    @Testcontainers
    public class GivenAPostgresDatabase extends GivenADatabase {

        @Container
        private static final PostgreSQLContainer<?> container = new PostgreSQLContainer<>("postgers:14");

        @DynamicPropertySource
        static void registerProperties(DynamicPropertyRegistry registry) {
            registry.add("spring.datasource.url", container::getJdbcUrl);
            registry.add("spring.datasource.username", container::getUsername);
            registry.add("spring.datasource.password", container::getPassword);
        }

        @Nested
        public class WhichIsEmptyPostgresDatabase extends WhichIsEmpty<RecipeJpaEntity, SpringDataRecipeRepository> {

            @Autowired
            private SpringDataRecipeRepository springDataRecipeRepository;

            @BeforeEach
            @Override
            protected void setup() {
                this.recipeRepository = new JpaRecipeRepository(springDataRecipeRepository);
                this.dataSource = springDataRecipeRepository;
            }
        }

        @Nested
        public class WhichContainsSomeRecipesPostgres extends WhichContainsSomeRecipes<RecipeJpaEntity, SpringDataRecipeRepository> {

            @Autowired
            private SpringDataRecipeRepository springDataRecipeRepository;

            @BeforeEach()
            void setup() {
                this.recipeRepository = new JpaRecipeRepository(springDataRecipeRepository);
                this.dataSource = springDataRecipeRepository;
            }
        }
    }
}

This does not work though due to errors pointed out in this issue. I would really see this as a valid use case to nicely organize the test cases which rely on presence of a database, and handle the database in different states. I am also not quite sure, what the elegant alternative to this issue would be.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants