2

我和我的同事目前正在向我们的旧版 Java EE5 代码库引入单元测试。我们主要使用 JUnit 和 Mockito。在编写测试的过程中,我们注意到我们的 EJB 中的几个方法很难测试,因为它们同时做了很多事情。

我对整个测试业务相当陌生,因此我正在寻找有关如何更好地构建代码或测试的见解。我的目标是写出好的测试而不用头疼。

这是我们的一种方法及其在管理消息队列的服务中的逻辑步骤的示例:

  • 消费消息

    • 确认PreviousDownloadedMessages

    • getNewUnreadMessages

    • addExtraMessages(取决于有些复杂的条件)

    • 将消息标记为已下载

    • 序列化消息对象

顶层方法目前暴露在接口中,而所有子方法都是私有的。据我了解,刚开始测试私有方法是不好的做法,因为只有公共接口才重要。

我的第一反应是公开所有子方法并单独测试它们,然后在顶级方法中确保它调用子方法。但是后来一位同事提到,将所有这些低级别方法与另一个方法暴露在同一级别可能不是一个好主意,因为这可能会引起混淆,并且其他开发人员可能会在他们应该使用顶级方法时开始使用一。我不能指责他的论点。

所以我在这里。

您如何协调公开易于测试的低级方法与避免使接口混乱?在我们的例子中,EJB 接口。

我在其他单元测试问题中读到应该使用依赖注入或遵循单一责任原则,但我在实践中遇到了麻烦。有人能指出如何将这种模式应用于上面的示例方法吗?

您会推荐其他通用 OO 模式或 Java EE 模式吗?

4

3 回答 3

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# 中。我希望你无论如何都能使用它:-)

于 2012-10-15T12:23:50.380 回答
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 的人来说,没有办法直接访问引擎,因此没有造成伤害的风险。另一方面,可以对引擎进行完全单元测试。

于 2012-10-16T07:08:16.843 回答
0

好吧,正如您所注意到的,对具体的高级程序进行单元测试非常困难。您还发现了两个最常见的问题:

通常程序被配置为使用特定资源,例如特定文件、IP 地址、主机名等。为了解决这个问题,您需要重构程序以使用依赖注入。这通常是通过向构造函数添加参数来替换 ahrdcoded 值来完成的。

测试大型类和方法也非常困难。这通常是由于测试复杂逻辑所需的测试数量的组合爆炸。为了解决这个问题,您通常会首先重构以获得更多(但更短)的方法,然后通过从原始类中提取几个类来尝试使代码更通用和可测试,每个类都有一个入口方法(公共)和几个实用程序方法(私有)。这本质上是单一责任原则。

现在您可以通过测试新类开始“向上”工作。这将容易得多,因为此时组合更容易处理。

在此过程中的某个时刻,您可能会发现使用以下设计模式可以大大简化代码:命令、复合、适配器、工厂、构建器和外观。这些是减少混乱的最常见模式。

旧程序的某些部分可能在很大程度上无法测试,要么是因为它们太笨拙,要么是因为它不值得麻烦。在这里,您可以满足于一个简单的测试,它只检查已知输入的输出是否没有改变。本质上是回归测试。

于 2012-10-15T12:16:12.380 回答