5

I'd like to test my Spring Boot command line application. I would like to mock certain beans (which I was able to do by annotating @ContextConfiguration(classes = TestConfig.class) at the top of my test class. In TestConfig.class, I override the beans that I would like to mock. I'd like Spring Boot to find the rest of the components. This seems to work.

The problem is that when I run the test, the entire application starts up as normal (ie. the run() method is called).

@Component public class MyRunner implements CommandLineRunner { //fields @Autowired public MyRunner(Bean1 bean1, Bean2 bean2) { // constructor code } @Override public void run(String... args) throws Exception { // run method implementation } 

I've tried to override the MyRunner @Bean and put it in TestConfig.class, but that doesn't seem to work. I understand that I'm loading the regular application context, but that's what I'd like to do (I think?) since I would like to re-use all (or most) of the @Component I created in my Application, and only mock a tiny subset.

Any suggestions?

EDIT:

Application.java

@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 
2
  • So to get this straight - You want to run from the command line with what's effectively your test context that will mock certain beans? Can you add the Application class (ie the class with the main method you are calling) to the question? Commented Feb 5, 2020 at 2:04
  • @stringy05 Edited the question with it. Basically I just want to write a test class where I can have all my components injected (except a few which I mock). The problem is when I run the test method, MyRunner gets injected and runs like on application startup, and for whatever reason mocking that class doesn't override it. Commented Feb 5, 2020 at 4:36

3 Answers 3

2

The answer was simpler than I thought. Add the MockBean in

@TestConfiguration public class TestConfig { @MockBean private MyRunner myRunner; } 

We can use the @MockBean to add mock objects to the Spring application context. The mock will replace any existing bean of the same type in the application context.

So MyRunner.run() is never called but I can still use all the other beans in my application.

Sign up to request clarification or add additional context in comments.

Comments

1

CommandLineRunners are ordinary beans with one exception:

After the application context is loaded, spring boot finds among all its beans the beans that implement this interface and calls their run method automatically.

Now, I would like you to ask to do the following:

  1. Remove ContextConfiguration from the test and place a breakpoint in constructor of MyRunner. The test should look like this:
@RunWith(SpringRunner.class) // if you're on junit 4, adjust for junit 5 if you need @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) public class MyTest { @Autowired private MyRunner myRunner; @Test public void testMe() { System.out.println("hello"); } } 
  1. Run the test and make sure that myRunner is loaded and its run method is called
  2. Now mock this class with MockBean annotation:
@RunWith(SpringRunner.class) // if you're on junit 4, adjust for junit 5 if you need @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) public class MyTest { @MockBean private MyRunner myRunner; @Test public void testMe() { System.out.println("hello"); } } 
  1. Run the test. Make sure that run method is not running. Your Application Context now should contain a mock implementation of your component.

  2. If the above works, then the problem is with TestConfig and ContextConfiguration annotation. In general when you run without ContextConfiguration you give spring boot test engine a freedom to mimic the application context started as if its a real application (with autoconfigurations, property resolution, recursive bean scanning and so forth). However if you put ContextConfiguration, spring boot test doesn't work like this - instead it only loads the beans that you've specified in that configuration. No Autoconfigurations, no recursive bean scanning happens for example.

Update

Based on OP's comment:

It looks like the MyRunner gets loaded when you put @ContextConfiguration because of component scanning. Since you have an annotation @Component placed on MyRunner class it can be discovered by Spring boot engine.

In fact there is a "dangerous" mix of two types of beans definitions here: 1. The beans defined with @Bean in @Configuration annotation 2. The beans found during component scanning.

Here is the question for you: If you don't want to mimic the application startup process and instead prefer to load only specific beans, why do you use @SpringBootTest at all? Maybe you can achieve the goal with:

@RunWith(SpringRunner.class) @ContextConfiguration(YourConfig.class) public class MyTest { ... } 

5 Comments

The problem is when I remove @ContextConfiguration, it picks up all of my application beans. The TestConfig.class was for my bean overrides. If I leave @ContextConfiguration, the application beans are actually all loaded correctly (including the ones I want to mock), but the CommandLineRunner bean is also loaded, and then runs the full app.
Noted, I've updated the answer in this case, please try my suggestion and update.
Thanks for all the help. I ended up finding out a pretty clean solution (see my own answer)
Cool, glad you've find the solution. You can also use @MockBean directly in the test, btw. :)
Thank you! I believe that last example of yours is the best way to do it. It's really hard to find that by googling because there are so many iterations on how to do this.
0

One way you could do this is to have 2 classes with the main method, one which sets up the "normal" context, and another that sets up the "mock" context:

Normal App Context, uses the usual Application

@SpringBootApplication(scanBasePackages = "com.example.demo.api") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean public Foo foo() { return new Foo("I am not mocked"); } @Bean public Bar bar() { return new Bar("this is never mocked"); } } 

Add another Application class that overrides the normal context with the mocked one

@SpringBootApplication(scanBasePackageClasses = {MockApplication.class, Application.class}) @Component public class MockApplication { public static void main(String[] args) { SpringApplication.run(MockApplication.class, args); } @Bean public Foo foo() { return new Foo("I am mocked"); } } 

When you run Application.main Foo will be "I am not mocked", when you run MockApplication.main() it will be "I am mocked"

1 Comment

This gives: java.lang.IllegalStateException: Found multiple @SpringBootConfiguration annotated classes

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.