我和我的同事目前正在为我们的旧版Java EE5代码库引入单元测试。我们主要使用JUnit和Mockito。在编写测试的过程中,我们注意到EJB中的几个方法很难测试,因为它们同时做了很多事情。
我对整个测试业务都很陌生,所以我正在寻找有关如何更好地构建代码或测试的见解。我的目标是在没有头痛的情况下编写好的测试。
这是我们的一个方法及其在管理消息队列的服务中的逻辑步骤的示例:
consumeMessages
acknowledgePreviouslyDownloadedMessages
getNewUnreadMessages
addExtraMessages(取决于某些复杂的条件)
markMessagesAsDownloaded
serializeMessageObjects
顶级方法当前在界面中公开,而所有子方法都是私有的。据我所知,开始测试私有方法是不好的做法,因为只有公共接口才有意义。
我的第一反应是将所有子方法公开并单独测试它们,然后在顶级方法中确保它调用子方法。但是一位同事提到,将所有这些低级方法暴露在与另一个级别相同的级别可能不是一个好主意,因为它可能会导致混淆,而其他开发人员可能会在他们应该使用顶级时开始使用一。我不能错过他的论点。
所以我在这里。
如何协调暴露易于测试的低级方法与避免混乱接口?在我们的例子中,EJB接口。
我读过其他单元测试问题,应该使用依赖注入或遵循单一责任原则,但我在实践中应用它时遇到了麻烦。有人会指出如何将这种模式应用于上面的示例方法吗?
您会推荐其他一般OO模式还是Java EE模式?
答案 0 :(得分:1)
依赖注入(DI)和单一责任原则(SRP)高度相关。
SRP基本上声明每个类应该只做一件事并将所有其他事项委托给不同的类。例如,您的serializeMessageObjects
方法应该被提取到自己的类中 - 让我们称之为MessageObjectSerializer
。
DI意味着将MessageObjectSerializer
对象作为参数注入(传递)到MessageQueue
对象 - 在构造函数中或在consumeMessages
方法的调用中。您可以使用DI框架来执行此操作,但我建议手动执行此操作以获取概念。
现在,如果您为MessageObjectSerializer
创建一个接口,您可以将其传递给MessageQueue
,然后您将获得该模式的完整值,因为您可以轻松创建模拟/存根测试。突然,consumeMessages
不必关注serializeMessageObjects
的行为方式。
下面,我试图说明这种模式。请注意,当您要测试consumeMessages时,您不必使用MessageObjectSerializer
对象。您可以创建一个模拟或存根,它完全按照您的意愿执行,并传递它而不是具体类。这确实使测试变得更加容易。请原谅语法错误。我没有访问Visual Studio,所以它是用文本编辑器编写的。
// THE MAIN CLASS
public class MyMessageQueue()
{
IMessageObjectSerializer _serializer;
//Constructor that takes the gets the serialization logic injected
public MyMessageQueue(IMessageObjectSerializer serializer)
{
_serializer = serializer;
//Also a lot of other injection
}
//Your main method. Now it calls an external object to serialize
public void consumeMessages()
{
//Do all the other stuff
_serializer.serializeMessageObjects()
}
}
//THE SERIALIZER CLASS
Public class MessageObjectSerializer : IMessageObjectSerializer
{
public List<MessageObject> serializeMessageObjects()
{
//DO THE SERILIZATION LOGIC HERE
}
}
//THE INTERFACE FOR THE SERIALIZER
Public interface MessageObjectSerializer
{
List<MessageObject> serializeMessageObjects();
}
编辑:抱歉,我的示例是在C#中。我希望你无论如何都可以使用它:-)
答案 1 :(得分:1)
乍一看,我想说我们可能需要引入一个新类,它可以1)公开可以进行单元测试的公共方法,但是2)不要在API的公共接口中公开。
举个例子,让我们假设您正在为汽车设计API。要实现API,您需要一个引擎(具有复杂的行为)。您想要完全测试您的引擎,但您不希望向汽车API的客户端公开详细信息(我所知道的关于我的汽车是如何按下启动按钮以及如何切换无线电频道)。
在那种情况下,我会做的是这样的事情:
public class Engine {
public void doActionOnEngine() {}
public void doOtherActionOnEngine() {}
}
public class Car {
private Engine engine;
// the setter is used for dependency injection
public void setEngine(Engine engine) {
this.engine = engine;
}
// notice that there is no getter for engine
public void doActionOnCar() {
engine.doActionOnEngine();
}
public void doOtherActionOnCar() {
engine.doActionOnEngine();
engine.doOtherActionOnEngine(),
}
}
对于使用Car API的人来说,无法直接访问引擎,因此不存在造成伤害的风险。另一方面,可以对发动机进行全面的单元测试。
答案 2 :(得分:0)
嗯,正如您所注意到的,单元测试具体的高级程序非常困难。您还确定了两个最常见的问题:
通常程序配置为使用特定资源,例如特定文件,IP地址,主机名等。要解决此问题,您需要重构程序以使用依赖项注入。这通常通过向构造函数添加替换ahrdcoded值的参数来完成。
测试大型类和方法也很困难。这通常是由于测试复杂逻辑所需的测试数量的组合爆炸造成的。为了解决这个问题,你通常会首先重构以获得更多(但更短)的方法,然后尝试通过从原始类中提取几个类,每个类都有一个单一的入口方法(公共)和几个实用程序来使代码更通用和可测试方法(私人)。这基本上是单一责任原则。
现在,您可以通过测试新类来“开始”工作。这将更加容易,因为此时组合更容易处理。
在此过程中的某些时候,您可能会发现使用这些设计模式可以极大地简化代码:Command,Composite,Adapter,Factory,Builder和Facade。这些是减少混乱的最常见模式。
旧程序的某些部分可能在很大程度上是不可测试的,要么是因为它们过于苛刻,要么是因为它不值得麻烦。在这里,您可以选择仅检查已知输入的输出未发生变化的简单测试。基本上是回归测试。