quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.53k stars 2.61k forks source link

Problems while creating integration tests with JUnit and RestAssured #41781

Open 1537592846 opened 1 month ago

1537592846 commented 1 month ago

Describe the bug

I was setting up integrations tests on my Quarkus application using JUnit and RestAssured. The way the application is setup, I'm trying to create basic entity, repository, service and resource that are all called by using the necessary methods and return errors as to be used by the front. The problem is during testing. The first entity I've replaced to use this works perfectly, and the tests implemented performed as expected, but when creating the tests for the second entity, the tests are failing to make the GET.

What I did to recreate the entity (Entity, repository, service, resource and test) was to copy/paste everything, changing the name, and if I start the server normally it works, but during the tests it returns a 404. If copying the first entity's test to the second, the test works, if copying the second entity's test to the first, it fails.

Expected behavior

The request made in the test of the second entity does not return 404.

Actual behavior

The request made in the test of the second entity does is currently returning 404.

How to Reproduce?

I'm not sure how to recreate it.

Output of uname -a or ver

Microsoft Windows [version 10.0.22631.3737]

Output of java -version

Java not installed, using IntelliJ version (corretto-17 Amazon Corretto 17.0.11)

Quarkus version or git rev

2.6.2.Final

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)

Additional information

This is the setup I made:

Basic entity:

@MappedSuperclass
open class BaseEntity(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0
) {
    protected val hashMultiplier: Int
        get() = 37

    @Override
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null) return false
        val oEffectiveClass = other.javaClass
        val thisEffectiveClass = this.javaClass
        if (thisEffectiveClass != oEffectiveClass) return false

        return this.toString() == other.toString()
    }

    @Override
    override fun toString(): String {
        return Gson().toJson(this) //Does not present as problem currently
    }

    override fun hashCode(): Int {
        return hashMultiplier * id.hashCode() +
                hashMultiplier * this::class.simpleName.hashCode() //This gets the name of the classes that implements it
    }
}

Entity example:

@Entity(name = "Foo")
data class Foo(
    var name: String,
    var description: String,
) : BaseEntity() {
    override fun hashCode(): Int {
        return this.hashCode() +
                this.hashMultiplier * name.hashCode() +
                this.hashMultiplier * description.hashCode()
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        if (!super.equals(other)) return false

        other as Foo

        if (name != other.name ) return false
        if (description!= other.description) return false

        return true
    }
}

Can't create a single base repository due to panache repositories shenanigans with @Default, so each class would have this code. BaseRepository:

@Repository
@Transactional
@RequestScoped
class FooPanacheRepository : PanacheRepository<Foo> {
    @Inject
    private lateinit var userRepository: UserRepository

    @Inject
    private lateinit var jwt: JsonWebToken

    @Inject
    private lateinit var logRepository: LogRepository

    fun searh(query: String, params: Map<String, Any>, sort: Sort? = null) = searchQuery(query, params, sort)
    fun searchUnique(query: String, params: Map<String, Any>, sort: Sort? = null): Foo{
        val data = searchQuery(query, params, sort)
        if (data.isEmpty()) throw InternalProjectException()
        if (data.size > 1) throw InternalProjectException()
        return data.first()
    }

    fun searchAll(sort: Sort? = null) = searchQuery("", mapOf(), sort)
    fun searchById(id: Long): Foo= searchUnique("id = :id", mapOf("id" to id))

    fun saveOrUpdate(entity: Foo): Foo=
        if (entity.id == 0L) save(entity) else update(entity)

    fun deleteThis(entity: Foo) = deleteEntity(entity)
    fun deleteThisById(id: Long) = deleteEntity(searchById(id))

    private fun searchQuery(query: String, params: Map<String, Any>, sort: Sort? = null): List<Foo> {
        try {
            return if (sort != null) {
                list(query, sort, params)
            } else {
                list(query, params)
            }
        } catch (ex: Exception) {
            throw InternalProjectException()
        }
    }

    private fun save(entity: Foo): Foo {
        try {
            persist(entity)
            log(entity)
            return entity
        } catch (ex: Exception) {
            throw InternalProjectException()
        }
    }

    private fun update(entity: Foo): Foo{
        try {
            getEntityManager().merge(entity)
            log(entity)
            return entity
        } catch (ex: Exception) {
            throw InternalProjectException()
        }
    }

    private fun deleteEntity(entity: Foo): Boolean {
        try {
            log(entity)
            return deleteById(entity.id)
        } catch (ex: Exception) {
            throw InternalProjectException()
        }
    }
}

And entity repositories use it like this:

@Repository
@Transactional
@ApplicationScoped
class FooRepository {
    @Inject
    private lateinit var repository: FooPanacheRepository 

    @CacheResult(cacheName = "foo_search")
    fun search(@CacheKey query: String, params: Map<String, Any>, @CacheKey sort: Sort? = null) =
        repository.search(query, params, sort)

//Other methods created as needed, following this example
}

Services are redirects from the endpoints in the resource, treating the data for the repositories, and resources are created to receive data from the front and send them directly to the service. Both of them have bases created for them implementing the default methods.

This is how the test is implemented:

@QuarkusTest
class FooResourceTest {
    @Inject
    lateinit var repository: FooPanacheRepository

    val url = "/foo"

    @Test
    fun testSearch() {
        val endpoint = "/bar=1" //Will change as needed
        val expectedData= repository.search() //Method using direct database access to get expected result
        val actualData: foo= `when`()
            .request("GET", url + endpoint)
            .then()
            .statusCode(200)
            .extract()
            .body().`as`(Foo::class.java)

        assertEquals(expectedData, actualData)
    }
}

This is replicated to the other tests, and can be changed if needed. Will post more details if needed.

quarkus-bot[bot] commented 1 month ago

/cc @geoand (kotlin)

geoand commented 1 month ago

cc @yrodiere

yrodiere commented 1 month ago

cc @yrodiere

Uh, do I miss some context or... ? Not sure why this is related to me.

Anyway:

Quarkus version or git rev

2.6.2.Final

I'm sorry but if that's only on Quarkus 2.6, I'm out :)

@1537592846 please try again on a maintained version of Quarkus (see https://endoflife.date/quarkus-framework; that would be Quarkus 3.8 or later I think).

If you still have a problem with a recent version of Quarkus, please provide a simple project with instructions on how to reproduce the problem, and someone will have a look.