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

时间:2019-05-10 12:54:36

标签: java unit-testing

我知道关于模拟和测试有很多问题,但是我发现没有什么问题可以完美地帮助我,所以我仍然在理解以下方面有困难:

如果我弄错了,请纠正我,但是据我所知,单元测试用于隔离地测试一个特定类的业务逻辑,并且如果有外部需要的任何对象,它们将被模拟。 因此,例如,如果我有一个简单城市公民的管理系统,该系统将公民添加到列表中并按其姓名返回公民(假设:公民仅包含一些基本的个人信息),如下所示:

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是必须模拟的外部功能,还是我只是为了测试目的而创建Citizen? 如果它们是模拟的,那么由于模拟对象不包含参数,我将如何测试该方法以按其名称获取对象?

5 个答案:

答案 0 :(得分:3)

在编写新代码(以及新的单元测试)或重构现有代码时,您希望能够一遍又一遍地运行单元测试,以确信现有功能是没有损坏。因此,单元测试必须稳定且快速

假设要测试的类依赖于某些外部资源,例如数据库。您更改了代码,单元测试突然失败。单元测试是否由于您刚引入的错误或由于外部资源不可用而中断?无法保证外部资源将始终可用,因此单元测试不稳定。模拟外部资源。

此外,连接到外部资源可能会花费太多时间。当您最终有成千上万个连接到各种外部资源的测试时,连接到外部资源的毫秒数加起来,这会降低您的速度。模拟外部资源。

现在添加CI / CD管道。在构建期间,单元测试失败。是外部资源不足还是您的代码更改破坏了某些东西?也许构建服务器无权访问外部资源?模拟外部资源。

答案 1 :(得分:2)

通常,模拟用于替换在测试中难以重复的实际调用。例如,假设ProcessClass进行REST调用以检索公民信息。对于简单的单元测试,将很难复制此REST调用。但是,您可以“模拟” RestTemplate并指定不同的返回类型,以确保您的代码可以处理200、403等。此外,您可以更改信息的类型,然后还可以测试您的代码以确保处理了错误的数据,例如信息丢失或为空。

在您的情况下,您实际上可以创建一个Citizen,然后测试该Citizen是列表中的对象还是getByName返回正确的对象。因此,在此示例中无需模拟。

答案 2 :(得分:2)

在您的特定示例中,不,您不需要模拟任何东西。

让我们专注于要测试的内容:

  1. 添加和检索一个市民的测试
  2. 添加2个市民,找回一个人
  3. 传递null而不是citizen,并确保您的代码不会中断。
  4. 添加两个同名公民,那么您会发生什么?
  5. 添加一个没有名字的公民。
  6. 添加一个名字为空的公民

等等等

您已经可以看到许多可以编写的不同测试。

为使它更有趣,您可以在您的类中添加一些代码,以公开公民列表的只读版本,然后可以检查列表是否包含正确的内容。

因此,在您的方案中,您不需要模拟任何东西,因为您对某种其他系统没有外部依赖性。公民似乎只是一个简单的模型类。

答案 3 :(得分:1)

  

如果它们是模拟的,由于模拟对象不包含参数,我将如何测试该方法按名称获取对象?

例如,您可以使用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"));
}

注意:

我建议嘲笑。假设您以这种方式实例化Citizen类的地方编写了100个测试

Citizen c = new Citizen();

,几个月后,您的构造函数更改为采用一个参数,这是一个对象本身,例如类City。现在,您必须返回并更改所有这些测试并编写:

City city = new City("Paris");
Citizen c = new Citizen(city);

如果您嘲笑Citizen,则无需这样做。

现在,因为它是POJO,并且其getName方法的构造函数可能不会更改,所以不应该进行模拟。

答案 4 :(得分:1)

回答问题的第一部分

  

如果现在我要对ProcessClass进行单元测试,我是否将Citizen视为必须模拟的外部功能,还是只是出于测试目的而创建Citizen?

在不了解Citizen的情况下很难说出来。但是,一般规则是,应出于某种原因进行模拟。充分的理由是:

  • 您不能轻易地使组件依赖(DOC)的行为符合测试的预期。
  • 调用DOC是否会引起任何非专业行为(日期/时间,随机性,网络连接)?
  • 测试设置过于复杂和/或需要大量维护(例如需要外部文件)
  • 原始DOC为您的测试代码带来了可移植性问题。
  • 使用原始DOC是否会导致构建/执行时间过长?
  • 是否存在使测试不可靠的DOC稳定性(成熟度)问题,或者更糟糕的是,甚至还没有DOC?

例如,您(通常)不模拟诸如sin或cos之类的标准库数学函数,因为它们没有上述任何问题。在您的情况下,您需要判断仅使用Citizen是否会导致上述任何问题。如果是这样,最好模拟一下,否则最好不要模拟。

相关问题