Mockito – 模拟 ApplicationContext

huangapple go评论71阅读模式
英文:

Mockito - mock ApplicationContext

问题

以下是您要翻译的内容:

"我有一个Springboot应用程序,它根据用户传递的输入参数在运行时从ApplicationContext中查找bean。对于这个方法,我试图编写Mockito测试用例,但它不起作用并抛出NullPointerException。

引导应用程序的类:

@SpringBootApplication
public class MyApplication {

	private static ApplicationContext appContext;
	
	public static void main(String[] args) {
		appContext = SpringApplication.run(MyApplication.class, args);
	}
	
	public static ApplicationContext getApplicationContext() {
		return appContext;
	}
	
}

我试图编写测试用例的类:

@Service
public class Mailbox {

	@Autowired
	MailProcessor processor;
	
	public void processUserInput(Envelope object) {
	
		processor.setCommand(MyApplication.getApplicationContext().getBean(object.getAction(), Command.class));
		processor.allocateEnvelopes(object);
		
	}
} 

我的测试用例如下:

@RunWith(MockitoJUnitRunner.class)
@SpringBootTest
@ActiveProfiles("test")
public class MailboxTest {

	@Mock
	MailProcessor processor;
	
	@InjectMocks
	Mailbox mailbox;
	
	@Test
	public void testProcessUserInput() {
		
		Envelope message = new Envelope();
		message.setAction("userAction");
		message.setValue("userInput");
		
		doNothing().when(processor).setCommand(any());
		doNothing().when(processor).allocateEnvelopes(any());
		
		mailbox.processUserInput(message);
		
		Mockito.verify(processor).allocateEnvelopes(any());
		
	}
	
	
}

每当我运行测试用例时,它会在Mailbox类中的processor.setCommand(MyApplication.getApplicationContext().getBean(object.getAction(), Command.class)); 处抛出NullPointerException。我如何模拟ApplicationContext的查找?我是否漏掉了任何模拟步骤?"

英文:

I have a Springboot application that looks up the bean from the ApplicationContext at runtime based on the input parameter passed by the user. For this method, I am trying to write Mockito test cases but it is not working and throws NullPointerException.

The class which bootstraps the application:

@SpringBootApplication
public class MyApplication {

	private static ApplicationContext appContext;
	
	public static void main(String[] args) {
		appContext = SpringApplication.run(MyApplication.class, args);
	}
	
	public static ApplicationContext getApplicationContext() {
		return appContext;
	}
	
}

Class for which I am trying to write the test cases:

@Service
public class Mailbox {

	@Autowired
	MailProcessor processor;
	
	public void processUserInput(Envelope object) {
	
		processor.setCommand(MyApplication.getApplicationContext().getBean(object.getAction(), Command.class));
		processor.allocateEnvelopes(object);
		
	}
} 

And my test case is as below:

@RunWith(MockitoJUnitRunner.class)
@SpringBootTest
@ActiveProfiles("test")
public class MailboxTest {

	@Mock
	MailProcessor processor;
	
	@InjectMocks
	Mailbox mailbox;
	
	@Test
	public void testProcessUserInput() {
		
		Envelope message = new Envelope();
		message.setAction("userAction");
		message.setValue("userInput");
		
		doNothing().when(processor).setCommand(any());
		doNothing().when(processor).allocateEnvelopes(any());
		
		mailbox.processUserInput(message);
		
		Mockito.verify(processor).allocateEnvelopes(any());
		
	}
	
	
}

Whenever I run the test cases it gives the NullPointerException at processor.setCommand(MyApplication.getApplicationContext().getBean(object.getAction(), Command.class)); in Mailbox class. How can I mock the ApplicationContext lookup? Am I missing any mocking step?

答案1

得分: 2

无法确定是否没有调试,但看起来 MyApplication.getApplicationContext() 返回了 null

不要将其存储在静态变量中,而应该尝试在需要的地方将 ApplicationContext 注入到您的 @Service 类中:

@Autowired
private ApplicationContext appContext;
英文:

Can't say for sure without debugging but it looks like MyApplication.getApplicationContext() is returning null.

Instead of storing it in a static variable you should try injecting the ApplicationContext in your @Service class where you need it:

@Autowired
private ApplicationContext appContext;

答案2

得分: 2

  1. 您的Mailbox服务不应该在任何层次上知道MyApplication。它是Spring Boot应用程序的入口点,您的业务逻辑不应该依赖于它。您可以直接将应用程序上下文注入到类中,如下所示:
@Service
public class Mailbox {
    private final ApplicationContext ctx;
    // ...
    public Mailbox(ApplicationContext ctx) {
        this.ctx = ctx;
    }
    // ...
}
  1. 即使解决了这个问题,总体来说,依赖于ApplicationContext也不是一个好主意。因为这样你就会变得依赖于Spring,而在Mailbox类中没有这样做的理由。不过,这样做后,该类将变得可以进行单元测试。

  2. 在解决方案方面:

在Spring中,您可以将一个Map<String, Command>注入到邮箱中(这是Spring的内置功能),以便映射的键将是bean的名称,也就是您信封操作的动作。以下是一个解决方案(在与注入无关的地方进行了简化,只是为了说明这个思路):

public interface Command {
    void execute();
}

@Component("delete") // 注意这个 "delete" 单词 - 它将成为邮箱中映射的键
public class DeleteMailCommand implements Command {
    @Override
    public void execute() {
        System.out.println("删除邮件");
    }
}

@Component("send")
public class SendMailCommand implements Command{
    @Override
    public void execute() {
        System.out.println("发送邮件");
    }
}

请注意,所有的命令都必须由Spring驱动(这似乎是您的情况)。现在,Mailbox将如下所示:

@Service
public class Mailbox {
    private final Map<String, Command> allCommands;
    private final MailProcessor processor;
    // 注意这个映射:它将是 {"delete" -> <类型为 DeleteMailCommand 的bean>, "send" -> <类型为 SendMailCommand 的bean>}
    public Mailbox(Map<String, Command> allCommands, MailProcessor mailProcessor) {
        this.allCommands = allCommands;
        this.processor = mailProcessor;
    }

    public void processUserInput(Envelope envelope) {
        Command cmd = allCommands.get(envelope.getAction());
        processor.executeCommand(cmd);
    }
}

这个解决方案很容易进行单元测试,因为您可以使用模拟命令来填充映射,无需处理应用程序上下文。

更新

我现在查看了您的测试,抱歉,它也不是很好:) @RunWith(MockitoJUnitRunner.class) 用于运行单元测试(完全不使用Spring)。在使用@SpringBootTest注解一起使用这个注解是没有意义的,因为它运行的是一个完整的系统测试:启动整个Spring Boot应用程序,加载配置等等。所以请确保您想要运行哪种类型的测试,并使用适当的注解。

英文:

Spring wise your code doesn't look good, and in particular is not unit testable. I'll explain:

  1. Your Mailbox service should not be aware of MyApplication at any level. It is an entry point of spring boot application and your business logic should not depend on that.
    Its true that you can inject the application context directly into the class. See an example below. Another (more "old-school") option here is using ApplicationContextAware interface in the Mailbox service (see this example). However, its still a bad code IMO:
@Service
public class Mailbox {
 private final ApplicationContext ctx;
 ...
 public Mailbox(ApplicationContext ctx) {
     this.ctx = ctx;
 }
 ...
}
  1. Even if you resolve it, in general its not a good idea to depend on the ApplicationContext as well. Because this way you become spring dependent and there is no reason to do that in the Mailbox class. The class will become unit testable though.

  2. In terms of resolution:

In spring you can inject a Map&lt;String, Command&gt; into the mailbox (Its a built-in feature in spring) so that the key of the map will be a bean name, exactly an action of your envelop.
So here is the solution (simplified in places not relevant to injection, just to illustrate the idea):

public interface Command {
 void execute();
}

@Component(&quot;delete&quot;) // note this &quot;delete&quot; word - it will be a key in the map in the Mailbox
public class DeleteMailCommand implements Command {
    @Override
    public void execute() {
        System.out.println(&quot;Deleting email&quot;);
    }
}

@Component(&quot;send&quot;)
public class SendMailCommand implements Command{
    @Override
    public void execute() {
        System.out.println(&quot;Sending Mail&quot;);
    }
}

Note, that all the commands must be driven by spring (which seems to be your case anyway).
Now, the Mailbox will look like this:

@Service
public class Mailbox {
    private final Map&lt;String, Command&gt; allCommands;
    private final MailProcessor processor;
    // Note this map: it will be [&quot;delete&quot; -&gt; &lt;bean of type DeleteMailCommand&gt;, &quot;send&quot; -&gt; &lt;bean of type SendMailCommand&gt;]
    public Mailbox(Map&lt;String, Command&gt; allCommands, MailProcessor mailProcessor) {
        this.allCommands = allCommands;
        this.processor = mailProcessor;
    }

    public void processUserInput(Envelope envelope) {
        Command cmd = allCommands.get(envelope.getAction());
        processor.executeCommand(cmd);

    }
}

This solution is easily unit testable, because you can populate the map with mock commands if you wish and there is no need to deal with the application context.

Update

I took a look on your test now, and it's also not really good, sorry Mockito – 模拟 ApplicationContext
@RunWith(MockitoJUnitRunner.class) is used to run unit tests (without spring at all). There is no point in placing this annotation in conjunction with @SpringBootTest which runs a full-fledged system test: starts the whole spring boot application, loads configurations and so forth.

So make sure what kind of tests you want to run and use the appropriate annotations.

答案3

得分: 1

尝试在第一个测试之前通过注入处理器来初始化邮箱对象。

邮箱 = new Mailbox(处理器);

英文:

Try initializing mailbox object by injecting processor before first test.

mailbox = new Mailbox(processor);

huangapple
  • 本文由 发表于 2020年8月10日 12:38:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/63334204.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定