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

jle*_*jle 6 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;
    }

}
Run Code Online (Sandbox Code Playgroud)

如果现在我想对我进行单元测试,我ProcessClass是否将其Citizen视为必须被模拟的外部功能,还是只是Citizen为了测试目的而创建一个?如果它们被模拟,我将如何测试通过名称获取对象的方法,因为模拟对象不包含参数?

And*_*w S 6

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

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

此外,连接到外部资源可能需要太多时间。当您最终有数千个连接到各种外部资源的测试时,连接到外部资源的毫秒数加起来,这会减慢您的速度。模拟外部资源。

现在添加 CI/CD 管道。在构建期间,单元测试失败。外部资源是否已关闭,或者您的代码更改是否破坏了某些内容?也许构建服务器无权访问外部资源?模拟外部资源。


Dir*_*ann 6

回答你问题的第一部分

如果现在我想对我的 ProcessClass 进行单元测试,我是否将 Citizen 视为必须模拟的外部功能,或者我只是创建一个 Citizen 来进行测试?

如果不了解更多信息,Citizen很难说清楚。然而,一般规则是,嘲笑应该是有原因的。好的理由是:

  • 您无法轻松地使依赖组件 (DOC) 按照测试的预期运行。
  • 致电 DOC 是否会导致任何非皮肤行为(日期/时间、随机性、网络连接)?
  • 测试设置过于复杂和/或维护密集(例如,需要外部文件)
  • 原来的DOC给你的测试代码带来了可移植性问题。
  • 使用原始 DOC 是否会导致构建/执行时间过长?
  • DOC 稳定性(成熟度)问题是否会导致测试不可靠,或者更糟糕的是,DOC 尚未可用?

例如,您(通常)不会模拟 sin 或 cos 等标准库数学函数,因为它们不存在任何上述问题。对于您的情况,您需要判断仅仅使用是否Citizen会导致上述任何问题。如果是这样,很可能最好嘲笑它,否则你最好不要嘲笑。


Ben*_*aye 2

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

您可以使用mockito模拟对 的调用getName,例如:

Citizen citizen = mock(Citizen.class);
when(citizen.getName()).thenReturn("Bob");
Run Code Online (Sandbox Code Playgroud)

这是您的方法的测试示例

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"));
}
Run Code Online (Sandbox Code Playgroud)

笔记:

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

Citizen c = new Citizen();
Run Code Online (Sandbox Code Playgroud)

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

City city = new City("Paris");
Citizen c = new Citizen(city);
Run Code Online (Sandbox Code Playgroud)

如果你一Citizen开始就嘲笑,那么你就不需要这样做。

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