Search code examples
javaspringmockitoflywayspring-test

@MockBean seems to rerun context creation and fails afterMigrate.sql


I have two integration test classes. One of these classes depends on the bean that is talking to external service, so I need to mock this bean, and @MockBean seems perfect for this. For injecting some seeds into DB I'm using flyway's afterMigrate.sql. So here is hot it looks like:

@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@SpringBootTest
@Transactional
@Rollback
class FooTest {

  @Autowired
  private MyService myService;
}

@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@SpringBootTest
@Transactional
@Rollback
class BarTest {

  @MockBean
  private ExternalService;

  @Autowired
  private MyService myService;
}

And afterMigrate.sql:

INSERT INTO my_table (id, name) VALUES (1, 'John Doe')

The problem appeared when I annotate the ExternatService as @MockBean as now the afretMigrate.sql runs twice and I'm getting the error:

java.lang.IllegalStateException: Failed to load ApplicationContext
....
Message    : ERROR: duplicate key value violates unique constraint "my_table_pkey"

When I'm changing the @MockBean to @Autowired the error is gone and context is created without any problems. Also, tests run without problems if I run BarTest separately. This is not the expected behavior for @MockBean as the documentation says:

Any existing single bean of the same type defined in the context will be replaced by the mock. If no existing bean is defined a new one will be added. Dependencies that are known to the application context but are not beans (such as those registered directly) will not be found and a mocked bean will be added to the context alongside the existing dependency.

It does not say that the context will be recreated.


Solution

  • Here is how I have resolved this issue (which I consider an issue).

    Solution 1: I have created a MockConfig class with that should create one mock for entire test suite:

    @Configration
    public class MockConfig {
    
      @Bean
      @Primary
      public ExternalService externalService() {
        return mock(ExternalService.class);
      }
    }
    

    And in the test, I'm just autowiring the external service:

    @Autowire
    private ExternalService externalService;
    

    But this solution has a problem, it will create a real bean then will override it with the mock bean. If your external service make a connection to the external resources on creation, and you don't need that then you will need another solution.

    Solution 2: Create a basic abstract class with @MockBean in it:

    @RunWith(SpringRunner.class)
    @ActiveProfiles("test")
    @SpringBootTest
    @Transactional
    @Rollback
    public abstract class BaseIntegrationTest {
      @MockBean
      ExternalService externalService;
    }
    

    And extend the integration test from this base class:

    class FooTest extends BaseIntegrationTest {
    
      @Autowired
      private MyService myService;
    }
    
    class BarTest extends BaseIntegrationTest {
    
      @Autowired
      private MyService myService;
    }
    

    Now the context won't refresh as it's always the same, and the real bean won't be created.