Отметьте unit test как ожидаемый сбой в JUnit

Как я могу пометить тест как ожидаемый сбой в JUnit 4?

В этом случае я хочу продолжить выполнение этого теста, пока что-то не будет исправлено вверх по потоку. Игнорирование теста идет слишком далеко, так как тогда я могу забыть об этом. Возможно, я смогу добавить аннотацию @expected и поймать исключение, созданное assertThat, но это также похоже на ожидаемое поведение.

Вот как выглядит мой текущий тест:

@Test
public void unmarshalledDocumentHasExpectedValue() 
{
    doc = unmarshaller.unmarshal(getResourceAsStream("mydoc.xml"));
    final ST title = doc.getTitle();
    assertThat(doc.getTitle().toStringContent(), equalTo("Expected"));
}

Это утверждение должно быть успешным, но из-за ошибки восходящего потока это не так. Тем не менее, этот тест правильный; он должен преуспеть. Практически все альтернативы, которые я нашел, вводят в заблуждение. Прямо сейчас я думаю, что @Ignore("This test should pass once fixed upstream") - мой лучший выбор, но я все равно должен помнить, чтобы вернуться к нему. Я бы предпочел, чтобы тестовый прогон.

В Python я могу использовать expectedFailure decorator:

class ExpectedFailureTestCase(unittest.TestCase):
    @unittest.expectedFailure
    def test_fail(self):
        self.assertEqual(1, 0, "broken")

С Qt QTestLib в С++ вы можете использовать QEXPECT_FAIL:

QEXPECT_FAIL("", "Will be fixed next version", Continue);
QCOMPARE(i, 42);

В обоих случаях выше, unit test работает, и я надеюсь, что это произойдет. Я что-то пропустил в JUnit?

Ответ 1

Я предполагаю, что вы хотите, чтобы тест прошел, если ваш assert не удался, но если утверждение завершается успешно, тогда тест также должен пройти.

Самый простой способ сделать это - использовать TestRule. TestRule дает возможность выполнять код до и после запуска тестового метода. Вот пример:

public class ExpectedFailureTest {
    public class ExpectedFailure implements TestRule {
        public Statement apply(Statement base, Description description) {
            return statement(base, description);
        }

        private Statement statement(final Statement base, final Description description) {
            return new Statement() {
                @Override
                public void evaluate() throws Throwable {
                    try {
                        base.evaluate();
                    } catch (Throwable e) {
                        if (description.getAnnotation(Deprecated.class) != null) {
                            // you can do whatever you like here.
                            System.err.println("test failed, but that ok:");
                        } else {
                            throw e;
                        }
                    }
                }
            };
        }
    }

    @Rule public ExpectedFailure expectedFailure = new ExpectedFailure();

    // actually fails, but we catch the exception and make the test pass.
    @Deprecated
    @Test public void testExpectedFailure() {
        Object o = null;
        o.equals("foo");
    }

    // fails
    @Test public void testExpectedFailure2() {
        Object o = null;
        o.equals("foo");
    }
}

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

Далее, в ExpectedFailure#apply(), когда я делаю base.evaluate(), я улавлю любой Throwable (который включает AssertionError), и если метод помечен аннотацией @Deprecated, я игнорирую ошибку. Вы можете выполнить любую логику, которую вы хотите решить, следует ли игнорировать ошибку или нет, на основе номера версии, некоторого текста и т.д. Вы также можете передать динамически определенный флаг в ExpectedFailure, чтобы он мог сбой для определенных номеров версий:

public void unmarshalledDocumentHasExpectedValue() {
    doc = unmarshaller.unmarshal(getResourceAsStream("mydoc.xml"));

    expectedFailure.setExpectedFailure(doc.getVersionNumber() < 3000);

    final ST title = doc.getTitle();
    assertThat(doc.getTitle().toStringContent(), equalTo("Expected"));
}

Дополнительные примеры см. в ExternalResource и ExpectedException

Игнорирование ожидаемого теста отказа, а не его передачи

Если вы хотите отметить, что вы тестируете как Ignored, а не Success, это становится немного сложнее, потому что тесты игнорируются до их выполнения, поэтому вам нужно ретроспективно отмечать тест как проигнорированный, который включал бы создание вашего собственного Runner, Чтобы дать вам начало, см. Мой ответ Как определить правило метода JUnit в пакете?. Или задайте другой вопрос.

Ответ 2

Я не совсем понимаю специфику вашего сценария, но вот как я обычно тестирую ожидаемый сбой:

Новый способ:

@Test(expected=NullPointerException.class)
public void expectedFailure() {
    Object o = null;
    o.toString();
}

для более старых версий JUnit:

public void testExpectedFailure() {
    try {
        Object o = null;
        o.toString();
        fail("shouldn't get here");
    }
    catch (NullPointerException e) {
        // expected
    }
}

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

Ответ 3

Как насчет явно ожидающих AssertionError?

@Test(expected = AssertionError.class)
public void unmarshalledDocumentHasExpectedValue() {
    // ...
}

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

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

Ответ 4

Один из вариантов - это тест как @игнорировать и помещать там текст, который может быть ошибкой и ожидающим исправления. Таким образом, он не будет работать. Затем он будет пропущен. Вы также можете использовать расширения в соответствии с вашими потребностями по-разному.

Ответ 5

Я взял Мэтью на шаг дальше и фактически реализовал аннотацию @Optional, которую вы могли бы использовать вместо аннотации маркера @Deprecated, которую он упоминает в своем ответе. Хотя прост, я поделюсь с вами кодом, может быть, это поможет кому-то:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Optional {

  /**
   * Specify a Throwable, to cause a test method to succeed even if an exception
   * of the specified class is thrown by the method.
   */
  Class<? extends Throwable>[] exception();
}

С простым изменением класса Matt ExpectedFailure:

public class ExpectedFailure implements TestRule {

  @Override
  public Statement apply(final Statement base, final Description description) {
    return statement(base, description);
  }

  private Statement statement(final Statement base, final Description description) {
    return new Statement() {

      @Override
      public void evaluate() throws Throwable {
        try {
          base.evaluate();
        } catch (Throwable e) {
          // check for certain exception types
          Optional annon = description.getAnnotation(Optional.class);
          if (annon != null && ArrayUtils.contains(annon.exception(), e.getClass())) {
            // ok
          } else {
            throw e;
          }
        }
      }
    };
  }
}

Теперь вы можете аннотировать свой тестовый метод с помощью @Optional, и он не будет терпеть неудачу, даже если данный тип исключения будет повышен (укажите один или несколько типов, которые вы хотели бы передать методу тестирования):

public class ExpectedFailureTest {

  @Rule public ExpectedFailure expectedFailure = new ExpectedFailure();

  // actually fails, but we catch the exception and make the test pass.
  @Optional(exception = NullPointerException.class)
  @Test public void testExpectedFailure() {
      Object o = null;
      o.equals("foo");
  }

}

[ОБНОВЛЕНИЕ]

Вы также можете переписать свои тесты с помощью JUnit org.junit.Assume вместо традиционного org.junit.Assert, если вы хотите, чтобы ваши тесты проходили, даже если предположение не выполняется.

От Assume JavaDoc:

Набор методов, полезных для формулировки предположений об условиях, в которых тест имеет смысл. Неисправность предположения не означает, что код нарушен, но что тест не дает никакой полезной информации. По умолчанию JUnit-бегун рассматривает тесты с ошибками при игнорировании.

Assume доступен с JUnit 4.4

Ответ 6

Используйте, если это возможно, классный класс вверх. Убейте его правильным результатом. При необходимости замените mock на реальный объект после исправления ошибки.