何时在单元测试中使用模拟对象

7
我知道有很多关于模拟和测试的问题,但我找不到完全帮助我的答案,所以我仍然不理解以下内容:

请纠正我如果我理解错误,但据我所见,单元测试用于测试一个特定类的业务逻辑并将需要从外部获取的任何对象进行模拟。
例如,如果我有一个简单城市的公民管理系统,该系统将公民添加到列表中并按其名称返回公民(假设:公民只包括几个基本个人信息),如下所示:
public class ProcessClass {

    ArrayList<Citizen> citizenList = new ArrayList<Citizen>();

    public void addCitizen(Citizen citizen) {
        citizenList.add(citizen);
    }

    public Citizen getByName(String name) {
        for (Citizen c : citizenList) {
            if (c.getName().equals(name)) {
                return c;
            }
        }
        return null;
    }

}

现在如果我想对我的 ProcessClass 进行单元测试,是否要将 Citizen 视为需要进行 Mock 的外部功能?或者只是为了测试目的而创建一个 Citizen 对象?如果需要 Mock,那么如何测试按名称获取对象的方法,因为 Mock 对象不包含参数?


2
如果你正在编写TestProcessClass,那么你不应该模拟ProcessClass,因为这就是你要测试的内容。我假设在这种情况下,Citizen是一个没有逻辑的数据类,所以没有必要模拟它。然而,如果你使用另一个带有逻辑的类,请考虑模拟它。 - DanielM
这真的取决于你想要测试什么。如果你想测试addCitizen方法,你不需要创建一个模拟公民对象。但是,如果你想测试getByName方法,我会建议你创建一个带有名称的模拟公民,并将其分配给你的ProcessClass,然后测试getByName。Mock主要用于服务。例如,如果你想测试一个访问数据库但没有实际数据库的类。 - Nicolas
3
如果Citizen只是一个简单的数据类,我会直接创建它们。一般而言,如果可以避免使用模拟对象就最好不要使用,因为模拟对象主要用于服务和依赖项。 - Boris the Spider
好的,这有点涉及到问题:由于Citizen不包含任何逻辑,它可以在ProcessClassTest中使用吗?将这些非逻辑类包含进来是否违反了隔离性? - jle
5个回答

6
当你编写新代码(以及新的单元测试)或重构现有代码时,你希望能够反复运行单元测试,以合理地确信现有功能未被破坏。因此,单元测试必须是稳定和快速的。
假设要测试的类依赖于某些外部资源,如数据库。你进行了代码更改,但单元测试突然失败。单元测试是否因为你刚刚引入的一个错误而失败,还是因为外部资源不可用?由于无法保证外部资源始终可用,因此单元测试是不稳定的。模拟外部资源。
此外,连接到外部资源可能需要太长时间。当你最终拥有成千上万个连接到各种外部资源的测试时,连接到外部资源所需的毫秒数会累加,这会减慢你的速度。模拟外部资源。
现在添加一个CI / CD管道。构建过程中,单元测试失败。外部资源是否关闭,还是你的代码更改导致了问题?也许构建服务器没有访问外部资源的权限?模拟外部资源。

6
回答你问题的第一部分:

如果现在我想对ProcessClass进行单元测试,我是应该将Citizen视为需要模拟的外部功能,还是仅仅为了测试目的创建一个Citizen?

不了解Citizen的具体情况很难说。然而,一般规则是,模拟应该有一个合理的原因。好的原因包括:
  • 您无法轻松使被依赖的组件(DOC)按照测试的意图运行。
  • 调用DOC是否会导致任何非确定性行为(日期/时间、随机性、网络连接)?
  • 测试设置过于复杂和/或维护成本高(例如需要外部文件)
  • 原始DOC会给测试代码带来可移植性问题。
  • 使用原始DOC是否导致不可接受的长构建/执行时间?
  • DOC的稳定性(成熟度)是否存在问题,从而使测试不可靠,或者更糟糕的是,DOC尚未发布?
例如,通常不会模拟标准库中的数学函数,如sin或cos,因为它们没有上述任何问题。在您的情况下,您需要判断只是使用Citizen是否会引起上述问题之一。如果是这样,很可能最好进行模拟,否则最好不要模拟。

3

通常来说,Mocking(模拟)被用于替换在测试中难以复制的实际调用。例如,假设ProcessClass通过REST调用检索Citizen信息。对于简单的单元测试,很难复制这个REST调用。但是,您可以“mock” RestTemplate并指定不同类型的返回,以确保您的代码将处理200、403等情况。此外,你还可以更改信息类型,以测试你的代码是否能够处理错误数据,如缺失或空信息。

在您的案例中,您实际上可以创建一个Citizen对象,并测试该Citizen是否为列表中的对象,或者getByName是否返回正确的对象。因此,在此示例中不需要进行模拟。


1
此外,我发现Baeldung在大多数情况下都很有帮助,这是他们关于测试的文章列表--> https://www.baeldung.com/junit,以及一篇关于Mockito的文章--> https://www.baeldung.com/mockito-series。干杯! - JavaJd

3
在你的具体示例中,不需要模拟任何东西。
让我们关注你要测试的内容:
1. 添加并检索一个公民的测试。 2. 添加两个公民,检索其中一个。 3. 将null作为公民传递,并确保你的代码不会崩溃。 4. 添加两个同名的公民,那么你会期望发生什么? 5. 添加一个没有名称的公民。 6. 使用null名称添加一个公民。
等等。
您已经可以看到许多不同的测试用例。为了使其更有趣,你可以向你的类添加一些代码,公开citizenList的只读版本,然后你可以检查你的列表是否包含完全正确的内容。
因此,在你的情况下,你不需要模拟任何东西,因为你没有对另一种类型的外部系统有依赖。Citizen似乎是一个简单的模型类,仅此而已。

2
如果它们被模拟了,我该如何测试通过其名称获取对象的方法,因为模拟对象不包含参数? 您可以使用Mockito来模拟调用getName。原始答案:最初的回答。
Citizen citizen = mock(Citizen.class);
when(citizen.getName()).thenReturn("Bob");

这是一个测试你的方法的例子,最初的回答:
ProcessClass processClass = new ProcessClass();

Citizen citizen1 = mock(Citizen.class);
Citizen citizen2 = mock(Citizen.class);
Citizen citizen3 = mock(Citizen.class);

@Test
public void getByName_shouldReturnCorrectCitizen_whenPresentInList() {
    when(citizen1.getName()).thenReturn("Bob");
    when(citizen2.getName()).thenReturn("Alice");
    when(citizen3.getName()).thenReturn("John");

    processClass.addCitizen(citizen1);
    processClass.addCitizen(citizen2);
    processClass.addCitizen(citizen3);

    Assert.assertEquals(citizen2, processClass.getByName("Alice"));
}

@Test
public void getByName_shouldReturnNull_whenNotPresentInList() {
    when(citizen1.getName()).thenReturn("Bob");

    processClass.addCitizen(citizen1);

    Assert.assertNull(processClass.getByName("Ben"));
}

注意:

我建议使用模拟。假设您编写了100个测试用例,其中实例化一个Citizen类的方式如下:

最初的回答

Citizen c = new Citizen();

几个月后,您的构造函数更改为接受一个参数,这个参数本身是一个对象,例如类City。现在您必须返回并更改所有这些测试,并编写以下内容: "最初的回答"。
City city = new City("Paris");
Citizen c = new Citizen(city);

如果一开始就使用了POJO类Citizen进行模拟,那么您就不需要再进行模拟了。现在,由于它是一个POJO类,而且其getName方法的构造可能不会改变,因此不进行模拟也应该是可以的。"最初的回答"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接