Search code examples
javaspring-bootliquibasespocktestcontainers

Liquibase executes migration after tests execution


I have very strange (for me) problem with testing my application, receiving this exception:

Caused by: liquibase.exception.MigrationFailedException: Migration failed for changeset db/changelog/inserts/insert_test_user.yaml::2::Jakub.Kolacz:
 Reason: liquibase.exception.DatabaseException: ERROR: duplicate key value violates unique constraint "user_email_key" Details: Key (email)=(john.doe@example.com) already exists. [Failed SQL: (0) INSERT INTO public.users (id, first_name, last_name, email, password, city, zip_code, street, building_number, flat_number) VALUES ('3', 'John', 'Doe', 'john.doe@example.com', 'password123', 'Warsaw', '00-001', 'Main St', '123', '45A')]

I want to test 'create' method:

    @PostMapping
public ResponseEntity<UserEntity> createUser(@RequestBody UserDto user) {
    try {
        return ResponseEntity.status(HttpStatus.CREATED).body(userService.save(user));
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
    }
}

I am using:

  • Spock as testing framework
  • Testcontainers (Postgresql) to simulate db for test
  • Liquibase as fms

When I run test method I receive "Failed to load ApplicationContext... blablabla" followed by exception that I wrote down above. I think it is problem with liquibase/testcontainers config. Let me show you code of test:

    class UserControllerITest extends BaseIntegrationTest {

    @Autowired
    UserRepository userRepository

    def "should return bad request when posted user's email is not unique"() {
    given: "a user and address DTO"
    def addressEntity = new AddressEntity(
            city: "Warsaw",
            zipCode: "00-001",
            street: "Main St",
            buildingNumber: "123",
            flatNumber: "45A"
    )

    def userDto = new UserEntity(
            id: 2L,
            firstName: "John",
            lastName: "Doe",
            email: "john.doe@example.com",
            password: "password123",
            address: addressEntity
    )

    def addressDto2 = new AddressDto(
            city: "Warsaw",
            zipCode: "00-001",
            street: "Main St",
            buildingNumber: "123",
            flatNumber: "45A"
    )

    def userDto2 = new UserDto(
            id: 3L,
            firstName: "John",
            lastName: "Doe",
            email: "john.doe@example.com",
            password: "password123",
            address: addressDto2
    )

    when: "create user endpoint is called"
    //userRepository.save(userDto)
    ResponseEntity response = restTemplate.postForEntity(addressToUseForTests + "/api/users", userDto2, UserDto)

    then: "response is 400 Bad request"
    response.statusCode == HttpStatus.BAD_REQUEST

}

I know this code may look a bit messy, later I'll explain why. My test class extends my custom class that's base configuration for all other integration test classes:

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
abstract class BaseIntegrationTest extends Specification {

@LocalServerPort
protected int port

@Shared
protected String addressToUseForTests

@Autowired
protected TestRestTemplate restTemplate

@Container
PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:15.3")
}

And config for test liquibase:

databaseChangeLog:
- includeAll:
  path: db/changelog/changeset/
- includeAll:
  path: db/changelog/inserts/

First path reaches table creating migrations, second is more important, files there inserts data used for test.

Now fun begins. When I run my test, I get exception with this stacktrace:

Failed to load ApplicationContext for [WebMergedContextConfiguration@73852720 testClass = 
com.honeybadgersoftware.cheappy.controller.UserControllerITest, locations = [], classes = 
[com.honeybadgersoftware.cheappy.CheappyApplication], contextInitializerClasses = [], 
activeProfiles = [], propertySourceLocations = ["classpath:application-test.properties"], 
propertySourceProperties = 
["org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true", 
"server.port=0"], contextCustomizers = 
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with 
name 'liquibase' defined in class path resource 
 [org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration$LiquibaseConfiguration.class]: liquibase.exception.MigrationFailedException: Migration failed for changeset db/changelog/inserts/insert_test_user.yaml::2::Jakub.Kolacz:
 Reason: liquibase.exception.DatabaseException: ERROR: duplicate key value violates unique constraint "user_email_key"
 Details: Key (email)=(john.doe@example.com) already exists. [Failed SQL: (0) INSERT INTO public.users
...
Caused by: liquibase.exception.LiquibaseException: 
liquibase.exception.MigrationFailedException: Migration failed for changeset 
db/changelog/inserts/insert_test_user.yaml::2::Jakub.Kolacz:
 Reason: liquibase.exception.DatabaseException: ERROR: duplicate key value violates unique 
constraint "user_email_key"
Details: Key (email)=(john.doe@example.com) already exists. [Failed SQL: (0) INSERT INTO 
public.users 
...
Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique 
constraint "user_email_key"
Szczegóły: Key (email)=(john.doe@example.com) already exists.

Why the test is messy? During debugging this test, I wanted to see what will happen when I remove insert migration from changelog and insert data manually in test (via userRepository.save() before executing post request).

Suprisingly (?) it returned expected exception (to be precise, it didnt reach http layer exception, but returned db layer exception:

 2023-08-11T00:52:59.968+02:00 ERROR 9180 --- [    Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: duplicate key value violates unique constraint "user_email_key"
 Szczegóły: Key (email)=(john.doe@example.com) already exists.

 could not execute statement [ERROR: duplicate key value violates unique constraint 
 "user_email_key"
  Szczegóły: Key (email)=(john.doe@example.com) already exists.] [update users setbuilding_number=?,city=?,flat_number=?,street=?,zip_code=?,email=?,first_name=?,last_name=?,passwo 
 rd=? where id=?]; SQL [update users setbuilding_number=?,city=?,flat_number=?,street=?,zip_code=?,email=?,first_name=?,last_name=?,passwo 
 rd=? where id=?]; constraint [user_email_key]
 org.springframework.dao.DataIntegrityViolationException: could not execute statement [ERROR: 
 duplicate key value violates unique constraint "user_email_key"

And this is expected exception. I also tried to test it manually (via sending requests to my local enviroment) and results were fine so I know that whole controller code works fine.

I tried to find something about it, but I cant find solution anywhere. I tried different configuration of testContainer, reached liquibase configuration, unfortunately I didnt find solution, not even single post/article about similar problem.

May some of you had problem like mine before?

Thanks for any help/tips.


Solution

  • According to article that @Leonard Brunings send me in the comment, solution for this issue was inserting these two lines into application-test.properties:

    spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
    spring.datasource.url=jdbc:tc:postgresql://localhost:5432/test
    

    Insead of classing Postgresql driver and data source url that I had:

    spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
    spring.datasource.driver-class-name=org.postgresql.Driver
    

    Once again big thanks to @Leonardo