Mockito - 模拟 ApplicationContext

Posted

技术标签:

【中文标题】Mockito - 模拟 ApplicationContext【英文标题】:Mockito - mock ApplicationContext 【发布时间】:2020-11-29 16:54:08 【问题描述】:

我有一个 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());
        
    
    
    

每当我运行测试用例时,它都会在 processor.setCommand(MyApplication.getApplicationContext().getBean(object.getAction(), Command.class));Mailbox 类中给出 NullPointerException。如何模拟 ApplicationContext 查找?我错过了任何模拟步骤吗?

【问题讨论】:

这是关于如何在 springboot 中测试的很好的指导 ***.com/questions/51789880/… 【参考方案1】:

Spring 明智的做法是,您的代码看起来不太好,尤其是不可单元测试。我来解释一下:

    您的Mailbox 服务在任何级别都不应该知道MyApplication。它是 Spring Boot 应用程序的入口点,您的业务逻辑不应依赖于此。 确实可以将应用程序上下文直接注入到类中。请参阅下面的示例。这里的另一个(更“老派”)选项是在Mailbox 服务中使用ApplicationContextAware 接口(参见this example)。但是,它仍然是 IMO 的错误代码:
@Service
public class Mailbox 
 private final ApplicationContext ctx;
 ...
 public Mailbox(ApplicationContext ctx) 
     this.ctx = ctx;
 
 ...

    即使您解决了它,通常也依赖 ApplicationContext 也不是一个好主意。因为这样你就变得依赖于 spring 并且没有理由在 Mailbox 类中这样做。不过,该类将成为可单元测试的。

    在分辨率方面:

在spring中,你可以在邮箱中注入Map<String, Command>(它是spring的内置功能),这样map的key就是一个bean名称,正是你信封的一个动作。 所以这里是解决方案(在与注入无关的地方进行了简化,只是为了说明这个想法):

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");
    

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

@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);

    

此解决方案易于进行单元测试,因为您可以根据需要使用模拟命令填充地图,而无需处理应用程序上下文。

更新

我现在看了你的测试,也不是很好,对不起:) @RunWith(MockitoJUnitRunner.class) 用于运行单元测试(完全没有弹簧)。将此注释与运行完整系统测试的@SpringBootTest 一起放置是没有意义的:启动整个 Spring Boot 应用程序,加载配置等等。

因此,请确定您要运行哪种测试并使用适当的注释。

【讨论】:

我喜欢你的评论。我一直在寻找一种在运行时查找 bean 的方法,但我选择了一条复杂的路径。您建议的方法要简单得多。我同意你的看法;业务逻辑不应该依赖于 main 方法。在我编写代码的那一刻,它没有点击。关于测试用例,删除了SpringBootTest,现在可以正常工作了。非常感谢你解释得这么好。【参考方案2】:

没有调试就不能确定,但​​看起来MyApplication.getApplicationContext() 正在返回null

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

@Autowired
private ApplicationContext appContext;

【讨论】:

【参考方案3】:

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

邮箱 = 新邮箱(处理器);

【讨论】:

以上是关于Mockito - 模拟 ApplicationContext的主要内容,如果未能解决你的问题,请参考以下文章

使用 Mockito 进行模拟测试的含义是啥 [重复]

使用 Mockito 时,模拟和间谍有啥区别?

Mockito 用 Spring 模拟:“传递给 verify() 的参数不是模拟!”

如何使用 mockito 模拟方法?

Junit/Mockito:选择使用模拟或集成测试运行测试

使用 Mockito 模拟静态方法