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?
Или вы можете вызвать закрытый метод, чтобы исправить это, когда проверили свое состояние отказа.