Как модульно тестировать конструкторы

#java #unit-testing

#java #модульное тестирование

Вопрос:

У меня есть класс, в который я добавляю модульные тесты. Класс имеет несколько конструкторов, которые принимают разные типы и преобразуют их в каноническую форму, которая затем может быть преобразована в другие типы.

 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);
}
  

Это устаревший класс, и я не могу изменить исходный класс.

Комментарии:

1. Наличие нескольких утверждений не должно вызывать у вас дискомфорта — это все академично. Если на практике это работает для вас и доступно в обслуживании, дерзайте.

2. Или рассмотрите возможность параметризации теста — один тест может проверять несколько входных значений, параметры могут включать как входные, так и ожидаемые выходные данные.

Ответ №1:

Похоже, у вас есть канонический способ получения «необработанного» значения ( toLong в данном случае) — так что просто проверьте правильность всех конструкторов при извлечении этого значения. Затем вы можете протестировать другие методы (такие как getString() ) на основе одного конструктора, поскольку вы знаете, что после завершения работы различных конструкторов все они оставляют объект в том же состоянии.

Это предполагает тестирование в виде «белого ящика», то есть вы знаете, что toLong на самом деле это простое отражение внутреннего состояния, поэтому можно протестировать это конструктор в тесте.

Комментарии:

1. Я думаю, что я пытался быть слишком скрытным в этом, чтобы быть практичным.

2. @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) {          
    }

}
  

Тест для проверки того, работают ли преобразования, может быть назначен получателям.

Ответ №3:

Тест для каждого конструктора кажется мне наиболее подходящим. Не бойтесь использовать длинные, сложные и многословные названия для ваших методов тестирования, это делает их очевидными и описательными.

 @Test
public void moneyConstructorThatTakesALong {
  

Ответ №4:

Я думаю, вы тратите слишком много времени на размышления об этом. Все ваши варианты работают просто отлично, поэтому просто выберите тот, который вам больше нравится. Не забывайте, что цель состоит в том, чтобы дать вам уверенность в том, что код будет работать так, как задумано / ожидаемо. Каждый из этих сценариев обеспечит это.

Лично в таком простом случае я бы выбрал один тестовый пример, который проверяет конструкторы. Это устраняет необходимость в чрезмерных и довольно громоздких именах методов.

Ответ №5:

Неплохая идея протестировать конструкторы, просто чтобы убедиться, что они принимают данные по мере необходимости, но если вам действительно не нравятся несколько утверждений, разделите их и назовите метод, с помощью которого они выполняют пример: CanContructorAcceptString или: CanConstructorAcceptNonLongStringValue

Что-то вроде этого.

Ответ №6:

У вас есть несколько входных данных (через конструкторы) и несколько выходных данных (через разные методы getX ()). Но количество элементов, которые у него есть внутри, кажется, меньше (в вашем примере 1 длинное значение). Не было бы проще сначала протестировать различные входные данные, создав x разных объектов, используя x разных конструкторов. Затем вы можете проверить, все ли они равны, используя реализованный метод equals(). Это можно сделать с помощью одного метода тестирования.

Затем вы можете проверять возможные методы получения один за другим, не используя все разные конструкторы.

Конечно, для этого требуется, чтобы вы реализовали (отдельно протестировали) метод 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... */ }
  

Комментарии:

1. Если конструктор ничего не делает, все эти тесты проходят, потому что money = 0.

2. Тесты testGet …() завершатся неудачей, если конструктор ничего не сделает.

Ответ №7:

Конструктор — это инициализатор объекта. Тестирование конструктора — это проверка факта инициализации. То есть проверка того, что поля объекта были инициализированы значениями, переданными конструктору. И это может быть проверено только отражением.