【问题标题】:How to unit test constructors如何对构造函数进行单元测试
【发布时间】:2011-05-26 12:44:22
【问题描述】:

我有一个要添加单元测试的课程。该类有几个构造函数,它们采用不同的类型并将它们转换为规范形式,然后可以转换为其他类型。

public class Money {
    public Money(long l) {
        this.value = l;
    }

    public Money(String s) {
        this.value = toLong(s);
    }

    public long getLong() {
        return this.value;
    }

    public String getString() {
        return toString(this.value);
    }
}

实际上它接受并转换为其他几种类型。

我正在尝试找出测试这些构造函数的最合适的方法。

每个构造函数和输出类型是否应该有一个测试:

@Test
public void longConstructor_getLong_MatchesValuePassedToConstructor() {
    final long value = 1.00l;

    Money m = new Money(value);
    long result = m.getLong();

    assertEquals(value, result);
}

这会导致很多不同的测试。如您所见,我正在努力为它们命名。

是否应该有多个断言:

@Test
public void longConstructor_outputsMatchValuePassedToConstructor() {
    final long longValue = 1.00l;
    final String stringResult = "1.00";

    Money m = new Money(longValue);

    assertEquals(longValue, m.getLong());
    assertEquals(stringResult, m.getString());
}

这有多个断言,这让我很不舒服。它还在测试 getString(以及通过代理 toString),但没有在测试名称中说明。给它们命名就更难了。

专注于构造函数是不是完全错误。我应该只测试转换方法吗?但是接下来的测试会错过toLong方法。

@Test
public void getString_MatchesValuePassedToConstructor() {
    final long value = 1.00;
    final String expectedResult = "1.00";

    Money m = new Money(value);
    String result = m.getLong();
    assertEquals(expectedResult, result);
}

这是一个遗留类,我不能更改原来的类。

【问题讨论】:

  • 拥有多个断言不应该让你感到不舒服——这都是学术性的。如果在实践中它对你有用并且是可维护的,那就去吧。
  • 或者考虑参数化测试——一个测试可以测试多个输入值,参数可以包括输入和预期输出。

标签: java unit-testing


【解决方案1】:

看起来您有一种获取“原始”值的规范方法(在本例中为toLong) - 因此,当您获取 那个 值时,只需测试所有构造函数是否正确.然后您可以基于单个构造函数测试其他方法(例如getString()),因为您知道一旦各种构造函数完成,它们都会使对象处于相同的状态。

这是假设有点白盒测试 - 即您知道 toLong 实际上是内部状态的简单反映,因此可以在测试中测试 + 构造函数。

【讨论】:

  • 我想我一直在试图让它变得有点太黑箱而不实用。
  • @ICR:我发现有很多关于网络测试的教条式的散文。我宁愿有大量实用但不完美的测试,也不愿只有一个实际上没有多大帮助的完美测试:)
【解决方案2】:

构造函数测试的预期结果是:实例已创建

按照这个想法,您可以将构造函数测试中的工作限制为纯实例化:

@Test public void testMoneyString() {
    try {
      new Money("0");
      new Money("10.0");
      new Money("-10.0");
    } catch (Exception e) {
      fail(e.getMessage());
    }
}

@Test public void testMoneyStringIllegalValue() {
    try {
      new Money(null);
      fail("Exception was expected for null input");
    } catch (IllegalArgumentException e) {          
    }

    try {
      new Money("");
      fail("Exception was expected for empty input");
    } catch (IllegalArgumentException e) {          
    }

    try {
      new Money("abc");
      fail("Exception was expected for non-number input");
    } catch (IllegalArgumentException e) {          
    }

}

验证转换是否可以分配给 getter 的测试。

【讨论】:

    【解决方案3】:

    每个构造函数的测试似乎对我来说最合适。不要害怕为您的测试方法使用冗长、详尽和冗长的名称,这会使它们变得明显且具有描述性。

    @Test
    public void moneyConstructorThatTakesALong {
    

    【讨论】:

      【解决方案4】:

      我认为你花了太多时间考虑它。您的所有选项都可以正常工作,因此只需选择您最喜欢的选项即可。不要忘记,目的是让您相信代码将按设计/预期工作。这些场景中的每一个都将提供这一点。

      就个人而言,在这种简单的情况下,我会使用一个验证构造函数的测试用例。它消除了对过多且相当笨拙的方法名称的需要。

      【讨论】:

        【解决方案5】:

        测试构造函数只是为了确保根据需要获取数据并不是一个坏主意,但是如果您真的不喜欢多个断言,请将它们拆分并使用它们正在执行的方法命名 示例:CanConstructorAcceptString 或:CanConstructorAcceptNonLongStringValue

        类似的东西。

        【讨论】:

          【解决方案6】:

          您有多个输入(通过构造函数)和多个输出(通过不同的 getX() 方法)。但是它内部的成员数量似乎更少(在您的示例中,1 个长值)。通过使用 x 不同的构造函数创建 x 不同的对象,首先测试不同的输入不是更容易吗?然后,您可以使用实现的 equals() 方法检查这些是否都相等。这可以在一个测试方法中完成。

          然后你可以一一检查可能的getter方法,而无需使用所有不同的构造函数。

          当然,这需要您实现(单独测试)equals 方法。

          在您的示例中,我将创建以下测试用例:

          @Test
          public void testEquals() {
              Money m1 = new Money(1);
              Money m2 = new Money(1);
              Money m3 = new Money(2);
          
              assertEquals(m1, m2);
              assertEquals(m2, m1);
              assertNotEquals(m1, m3);
              assertNotEquals(m3, m1);
              assertNotEquals(m1, null);
          }
          
          private void testConstructors(long lValue, String sValue) {
              Money m1 = new Money(lValue);
              Money m2 = new Money(sValue);
          
              assertEquals(m1, m2);
          }
          
          @Test
          public void testConstructorsPositive() {
              testConstructors(1, "1");
          }
          
          @Test
          public void testConstructorsNegative() {
              testConstructors(-1, "-1");
          }
          
          @Test
          public void testConstructorsZero() {
              testConstructors(0, "0");
          }
          
          @Test
          public void testGet...() { /* etc... */ }
          

          【讨论】:

          • 如果构造函数什么都不做,所有的测试都通过了,因为money = 0。
          • 如果构造函数不做任何事情,testGet...() 测试将失败。
          【解决方案7】:

          构造函数是一个对象初始化器。构造函数测试是对初始化事实的测试。也就是说,验证对象的字段是否由传递给构造函数的值初始化。而这只能通过反思来验证。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2010-09-26
            • 2012-06-28
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-12-03
            相关资源
            最近更新 更多