英文:
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
- 您的
Mailbox
服务不应该在任何层次上知道MyApplication
。它是Spring Boot应用程序的入口点,您的业务逻辑不应该依赖于它。您可以直接将应用程序上下文注入到类中,如下所示:
@Service
public class Mailbox {
private final ApplicationContext ctx;
// ...
public Mailbox(ApplicationContext ctx) {
this.ctx = ctx;
}
// ...
}
-
即使解决了这个问题,总体来说,依赖于
ApplicationContext
也不是一个好主意。因为这样你就会变得依赖于Spring,而在Mailbox
类中没有这样做的理由。不过,这样做后,该类将变得可以进行单元测试。 -
在解决方案方面:
在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:
- Your
Mailbox
service should not be aware ofMyApplication
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 usingApplicationContextAware
interface in theMailbox
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;
}
...
}
-
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.
-
In terms of resolution:
In spring you can inject a Map<String, Command>
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("delete") // note this "delete" word - it will be a key in the map in the Mailbox
public class DeleteMailCommand implements Command {
@Override
public void execute() {
System.out.println("Deleting email");
}
}
@Component("send")
public class SendMailCommand implements Command{
@Override
public void execute() {
System.out.println("Sending Mail");
}
}
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<String, Command> allCommands;
private final MailProcessor processor;
// Note this map: it will be ["delete" -> <bean of type DeleteMailCommand>, "send" -> <bean of type SendMailCommand>]
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);
}
}
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
@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);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论