【问题标题】:When to use mock objects in unit tests何时在单元测试中使用模拟对象
【发布时间】:2019-05-10 12:54:36
【问题描述】:

我知道有很多关于模拟和测试的问题,但我没有找到任何对我有完美帮助的问题,所以我仍然无法理解以下内容:

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

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? 如果它们被模拟,我将如何测试该方法以通过其名称获取对象,因为模拟对象不包含参数?

【问题讨论】:

  • 如果你写的是TestProcessClass,你不应该模拟ProcessClass,因为它是你正在测试的。我假设在这种情况下Citizen 是一个没有逻辑的数据类,所以没有必要模拟它。但是,如果您使用另一个具有逻辑的类,请考虑模拟它
  • 这真的取决于您要测试的内容。如果您尝试测试addCitizen 方法,则无需创建模拟公民对象。但是,如果您尝试测试getByName 方法,我会说您可以创建一个具有名称的模拟公民并将其分配给您的ProcessClass,然后测试getByName。模拟是大多数使用的白色服务。例如,如果您想测试一个在没有实际数据库的情况下访问数据库的类。
  • 如果Citizen 只是一个简单的数据类,我会创建它们。一般来说,如果可以的话,最好避免嘲笑。模拟对服务和依赖项很有用。
  • 好吧,这有点牵涉到这个问题:由于 Citizen 不包含任何逻辑,它可以在 ProcessClassTest 中使用,或者包含这些非逻辑类是否违反隔离像他们一样?

标签: java unit-testing


【解决方案1】:

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

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

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

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

【讨论】:

    【解决方案2】:

    回答你问题的第一部分

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

    如果不知道更多关于Citizen 的信息,很难说。但是,一般规则是,应该出于某种原因进行嘲笑。充分的理由是:

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

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

    【讨论】:

      【解决方案3】:

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

      在您的情况下,您实际上可以创建一个 Citizen,然后测试 Citizen 是列表中的一个对象,或者 getByName 返回正确的对象。所以在这个例子中不需要模拟。

      【讨论】:

      【解决方案4】:

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

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

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

      等等等等

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

      为了让它更有趣,你可以在你的类中添加一些代码来公开你的citizenList的只读版本,然后你可以检查你的列表是否包含正确的东西。

      因此,在您的场景中,您不需要模拟任何东西,因为您没有对其他某种系统的外部依赖。 Citizen 似乎是一个简单的模型类,仅此而已。

      【讨论】:

        【解决方案5】:

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

        您可以模拟对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);
        

        如果你一开始就嘲笑Citizen,你就不需要了。

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-06-18
          • 1970-01-01
          • 1970-01-01
          • 2019-01-22
          • 1970-01-01
          • 2023-03-22
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多