NUnit - очистка после сбоя теста

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

Можно ли обнаружить, что один из тестов не прошел и выполнить какую-то очистку?

Мы не хотим писать код очистки в каждом тесте, мы уже делаем это сейчас. Я хотел бы выполнить очистку в Teardown, но только если тест не пройден, так как очистка может быть дорогой.

Обновление: чтобы уточнить - я хотел бы, чтобы тесты были простыми и НЕ содержали никакой логики очистки или обработки ошибок. Я также не хочу выполнять сброс базы данных при каждом запуске теста - только если тест не пройден. И этот код, вероятно, должен быть выполнен в методе Teardown, но я не знаю ни одного способа получить информацию, если тест, который мы в настоящее время разрываем, провалился или прошел успешно.

Обновление 2:

        [Test]
        public void MyFailTest()
        {
            throw new InvalidOperationException();
        }

        [Test]
        public void MySuccessTest()
        {
            Assert.That(true, Is.True);
        }

        [TearDown]
        public void CleanUpOnError()
        {
            if (HasLastTestFailed()) CleanUpDatabase();
        }

Я ищу реализацию HasLastTestFailed()

11 ответов

Решение

Эта идея заинтересовала меня, поэтому я немного покопался. NUnit не имеет этой возможности из коробки, но есть целая инфраструктура расширяемости, поставляемая с NUnit. Я нашел эту замечательную статью о расширении NUnit - это была хорошая отправная точка. Поэкспериментировав, я нашел следующее решение: метод, украшенный CleanupOnError Атрибут будет вызван, если один из тестов в приборе не пройден.

Вот как выглядит тест:

  [TestFixture]
  public class NUnitAddinTest
  {
    [CleanupOnError]
    public static void CleanupOnError()
    {
      Console.WriteLine("There was an error, cleaning up...");
      // perform cleanup logic
    }

    [Test]
    public void Test1_this_test_passes()
    {
      Console.WriteLine("Hello from Test1");
    }

    [Test]
    public void Test2_this_test_fails()
    {
      throw new Exception("Test2 failed");
    }

    [Test]
    public void Test3_this_test_passes()
    {
      Console.WriteLine("Hello from Test3");
    }
  }

где атрибут просто:

  [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
  public sealed class CleanupOnErrorAttribute : Attribute
  {
  }

А вот как это выполняется из надстройки:

public void RunFinished(TestResult result)
{
  if (result.IsFailure)
  {
    if (_CurrentFixture != null)
    {
      MethodInfo[] methods = Reflect.GetMethodsWithAttribute(_CurrentFixture.FixtureType,
                                                             CleanupAttributeFullName, false);
      if (methods == null || methods.Length == 0)
      {
        return;
      }

      Reflect.InvokeMethod(methods[0], _CurrentFixture);
    }
  }
}

Но вот сложная часть: надстройка должна быть помещена в addins каталог рядом с бегуном NUnit. Мой был помещен рядом с бегуном NUnit в каталоге TestDriven.NET:

C:\Program Files\TestDriven.NET 2.0\NUnit\addins

(Я создал addins справочник, его там не было)

РЕДАКТИРОВАТЬ Другое дело, что метод очистки должен быть static!

Я взломал простой плагин, вы можете скачать исходники с моего SkyDrive. Вам нужно будет добавить ссылки на nunit.framework.dll, nunit.core.dll а также nunit.core.interfaces.dll в соответствующих местах.

Несколько замечаний: класс атрибута может быть размещен в любом месте вашего кода. Я не хотел размещать его в той же сборке, что и сам надстройка, потому что он ссылается на два Core NUnit сборок, поэтому я поместил его в другую сборку. Просто не забудьте изменить строку в CleanAddin.cs, если вы решили поставить его где-нибудь еще.

Надеюсь, это поможет.

Начиная с версии 2.5.7, NUnit позволяет Teardown определять, не прошел ли последний тест. Новый класс TestContext позволяет тестам получать доступ к информации о себе, включая TestStauts.

Для получения более подробной информации, пожалуйста, обратитесь к http://nunit.org/?p=releaseNotes&r=2.5.7

[TearDown]
public void TearDown()
{
    if (TestContext.CurrentContext.Result.Status == TestStatus.Failed)
    {
        PerformCleanUpFromTest();
    }
}

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

Я бы порекомендовал изменить код так, чтобы у вас были включены транзакции базы данных, и в конце теста просто верните базу данных в исходное состояние (например, отмените транзакцию, которая представляет ваши юнит-тесты).

Да, есть. Вы можете использовать атрибут Teardown, который будет разрушаться после каждого теста. Вы хотели бы применить этот скрипт для "сброса" базы данных, который у вас есть, и демонтировать и перенастроить до и после каждого теста.

Этот атрибут используется внутри TestFixture для предоставления общего набора функций, которые выполняются после запуска каждого метода тестирования.

Обновление: основываясь на комментариях и обновлении вопроса, я бы сказал, что вы можете использовать атрибут teardown и частные переменные, чтобы указать, должно ли срабатывать содержимое метода.

Хотя я также видел, что вам не нужна сложная логика или код обработки ошибок.

Учитывая это, я думаю, что стандартная установка / разборка будет работать лучше для вас. Не имеет значения, есть ли ошибка, и вам не нужно иметь какой-либо код обработки ошибок.

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

Один из вариантов, не упомянутых до сих пор, заключается в том, чтобы обернуть тест в объект TransactionScope, поэтому не имеет значения, что произойдет, поскольку тест никогда ничего не фиксирует в БД.

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

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

Редактировать - я только что заметил, что ответ Рэй Хейс также похож на мой.

Вы можете добавить [TearDown] метод с
if (TestContext.CurrentContext.Result.Status != TestStatus.Passed)
некоторый код, который будет выполнен, если тест не пройден.

Как насчет использования блока Try-Catch, перебрасывающего пойманную исключительную ситуацию?

try
{
//Some assertion
}
catch
{
     CleanUpMethod();
     throw;
}

Другой вариант - иметь специальную функцию, которая будет генерировать ваши исключения, которая устанавливает переключатель в testfixture, который сообщает, что произошло исключение.

public abstract class CleanOnErrorFixture
{
     protected bool threwException = false;

     protected void ThrowException(Exception someException)
     {
         threwException = true;
         throw someException;
     }

     protected bool HasTestFailed()
     {
          if(threwException)
          {
               threwException = false; //So that this is reset after each teardown
               return true;
          }
          return false;
     }
}

Тогда, используя ваш пример:

[TestFixture]
public class SomeFixture : CleanOnErrorFixture
{
    [Test]
    public void MyFailTest()
    {
        ThrowException(new InvalidOperationException());
    }

    [Test]
    public void MySuccessTest()
    {
        Assert.That(true, Is.True);
    }

    [TearDown]
    public void CleanUpOnError()
    {
        if (HasLastTestFailed()) CleanUpDatabase();
    }
}

Единственная проблема заключается в том, что трассировка стека приведет к CleanOnErrorFixture

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

Вы также сможете намного лучше протестировать свое ExceptionHandling.

Я не говорю, что это отличная идея, но она должна работать. Помните, что ошибки утверждений являются лишь исключениями. Также не забывайте, что есть также атрибут [TestFixtureTearDown], который запускается только один раз после выполнения всех тестов в приборе.

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

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


[TestFixture]
public class Tests {
     private bool testsFailed = false;

     [Test]
     public void ATest() {
         try {
             DoSomething();
             Assert.AreEqual(....);
         } catch {
            testFailed = true;
         }
     }

     [TestFixtureTearDown]
     public void CleanUp() {
          if (testsFailed) {
              DoCleanup();
          }
     }
}

Как это терпит неудачу? Можно ли поставить его в блок try (do test) / catch (исправить поврежденный db) / finally?

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

Другие вопросы по тегам