Pull to refresh

Comments 26

На мой взгляд, решение чуть менее, чем полностью ужасное. Это выглядит как костыль, код становится очень плохо читаемым.
В целом я согласен, что бывает не удобно, когда первый ассерт упал, а остальные не проверились. Я бы на вашем месте взял исходники NUnit, взял оттуда класс Assert, на его основе сделал, класс, который не выполняет, а только запоминает все ассерты. А в конце тест метода, нужно вызвать что-то вроде Assert.Run(). И в этом методе класс уже будет проверять все условия и выдаст общий результат.
Конечно, это потребует чуть больше, чем 20 строк кода, но тесты внешне будут точно такими же как раньше, только придётся в конце добавить соответствующий вызов.
И конечно, если поиграться с кодогенерацией, профилированием и т.п. Можно вообще ничего не менять в коде, а всё применять уже к бинарнику, но это более магический путь, не люблю такие заморочки. Лучше, когда работа кода очевидна, по самому коду.
Модификация исходников NUnit мне представляется еще более костыльным решением. Прежде всего потому, что это ведь будет не нормальный reuse, а по сути copy-paste со всеми его вытекающими в виде убитой поддерживаемости и прочего: обновится NUnit, придется вручную обновлять свой код, чтобы оставаться up-to-date.

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

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

И спасибо за фидбэк по поводу читаемости, взгляд со стороны это всегда полезно :)
Я не говорил о прямой модификации. Нет, никакой проблемы, создать отдельный класс, который скопирует интерфейс Assert-а, но внутри будет накапливать данные, а потом вызовет обычный класс. Чтобы не было проблем с многопоточностью я вижу два подхода:
1. Простой. Делаем наш класс не статическим, так что в каждом тест методе придётся создавать его экземпляр (либо в SetUp), это нормально и декларативно и всё такое. Но мне лично, всё таки не очень нравится каждый раз писать стандартный код.
2. Магический. Внутри нашего статического класса использовать привязку к вызывающему методу и потоку и хранить данные по этим ключам. Таким образом, данные для каждого теста будут независмы. Также, раз у нас будет метод проверки всех ассертов, в нём же можно очищать кэш, чтобы тесты не жрали память. Магический метод внешне будет удобнее, но опять же большой минус, его магичности. Для какого-нить домашнего проекта (либо проекта с моей небольшой командой), я бы использовал этот метод. Для публичных проектов, лучше вообще не использовать всяких домашних заготовок, т.к. важно, чтобы большинство сходу понимало код.
С описанного вами простого способа у меня как раз началась цепочка рассуждений, которая привела к решению описанному в посте, отдельный не статический класс. В SetUp кстати записывать мне показалось некрасивым, т.к. это нужно далеко не во всех тест-методах, но это и не суть.

Магический способ очень интересный. Как раз недавно использовали CallContext для управления Lifetime некоторых DAL-объектов, понравилось. Но здесь это пожалуй оверкилл и слишком комплексно, как вы и говорите.

А почему в целом от копивания интерфейса Assert я пришел к классу-аккумулятору:
1. Это все же отчасти содержит копирование;
2. Ну очень много методов там придется оборачивать, если основательно делать, а время не резиновое;
3. Самое неприятное: внутри каждого метода-обертки будет одинаковый абсолютно код вида try { Assert.Foo(args); } catch { /* запомнить */ }, что ужасно однообразно и громоздко на мой взгляд.

По поводу стандартного кода каждый раз: много ассертов в одном тесте все же не столь часто нужно, а когда нужно, то по-моему с описанным классом как раз получается декларативно и понятно, что делается, хоть читабельность внешнего кода возможно и страдает несколько из-за лямбд.
Методам Accumulate и Release полезно будет сделать возвращаемое значение типа AssertsAccumulator и возвращать тот же объект.
В вашем примере текст проверки будет выглядеть читабельнее.

new AssertsAccumulator()
  .Accumulate(() => Assert.That(signInResult.IsSuccess));
  .Accumulate(() => Assert.That(signInResult.Value, Is.Not.Null));
  .Accumulate(() => Assert.That(signInResult.Value.Username, Is.EqualTo(TestUsername)));
  .Accumulate(() => Assert.That(signInResult.Value.Password, Is.EqualTo(HashedTestPassword)));
  .Release();
Asserts.Accumulate(new []{
  () => Assert.That(signInResult.IsSuccess),
  () => Assert.That(signInResult.Value, Is.Not.Null),
  () => Assert.That(signInResult.Value.Username, Is.EqualTo(TestUsername)),
  () => Assert.That(signInResult.Value.Password, Is.EqualTo(HashedTestPassword))
});
var expectedResult = new SignInResult{
   IsSuccess = true,
   Value = new SignInResultValue
   {
       UserName = TestUserName,
       Password = HasghedTestPassword
,  }
}

Assert.Equals(expectedResult, signInResult);


Пример, который вы приводите все таки слишком прост.
Он легко разбивается на 4 изолированных теста.

Думаю, многим была бы интересна именно первоначально сложная проблема, которая и требует подобного решения.
В настоящем примере короткий код конструктора был покрыт в тесте десятком немного, но разных ассертов, семантически проверящих одно и то же. 10 методов для такого делать представлялось неудобным.
Еще один минус подобного решения — не видно в каким месте произошла ошибка.
Т.е. определять сломанный тест придется только по его наименованию.
Это не так, всегда ведь есть параметр message у любого метода Assert'a для вывода развернутого сообщения. Эти сообщения в итоговом выводе аккумулятора будут сохранены. Даже в топике, например, в тесте конструктора, каждое из падений будет ясно видно по сообщению об ошибке. В примере с логином я просто поленился писать сообщения, что вообще говоря нехорошо делать :).
Я имел в виду, что при возникновении исключительной ситуации можно в один клик перейти на строку кода, в которой данное исключение произошло.

В предлженом же варианте, насколько я понимаю, это не возможно.
А-а, вы дебаг теста имели ввиду. Неправильно вас понял в первый раз. Тогда да, вы правы, отладка возможно будет менее удобной. Но зато, опять же, если будет несколько ошибок, то увидите все сразу в конце, а не вторую лишь после исправления первой и ребилда. Это особенно важно, если по тем или иным причинам проект у вас целиком билдится и отлаживается не локально, а в CI системе, например.
Я конечно понимаю, что проекты бывают разные, и некоторые тесты могут работать довольно продолжительное время, но я придерживаюсь правила, что сборка из репозитария должна всегда работать и в команде сломанные билды не поощряем.

С другой стороны, по моему личному мнению, у тестов может быть два состояния: красный или зеленый. Или пришли все проверки внутри теста либо нет. Я считаю, что анализировать по текстовым сообщениям какая конкретно часть теста упала будет не удобно.

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

[TestCase(0, 0, 0)]
[TestCase(2, 0, 2)]
[TestCase(15, 1, 3)]
[TestCase(36, 3, 0)]
public void ConstructorSuccess(int ctorParam, int expectedFeet, int expectedRemainderInches)
{
    var testBox = new Size(ctorParam);
    Assert.That(testBox.Feet, Is.EqualTo(expectedFeet));
    Assert.That(testBox.RemainderInches, Is.EqualTo(expectedRemainderInches));
}
Про параметризированный тест для данного примера вы правы, действительно рациональнее и компактнее получается. Спасибо, перепишу в проекте место, по которому этот пример сделан :). Такие случаи, соответственно, отпадают из области использования класса.

А в целом, по поводу тестов на удаленной машине: их в любом случае нужно прогонять в окружении, где развернут проект, и их результат от локального может там отличаться из-за разных факторов вроде часовых поясов и прочих особенностей конкретного окружения. Поэтому анализировать результаты по текстовым сообщениям приходится в любом случае время от времени, а чем больше информации получается из них, тем лучше, лишний билд и запуск тестов может съесть кучу времени. То, что состояния у теста два — это так, но когда оно красное, то лучше, когда ошибки выведены все. На мой взгляд, это как с компиляцией, она тоже может быть или успешная, или нет, но нехорошо же, если вам при неуспешной ошибку вам выводят только одну, а остальные только при следующей.
Спасибо за пост и с Новым Годом!
Я тоже копал на эту тему:

        [Test]
        public void TestSearchGoogleForThisIsATestWorkaround04()
        {
            // Actual: "this is a test - Поиск в Google"
            // * Arrange
            IWebDriver selenium = new InternetExplorerDriver();
            var googlePage = new GoogleSearchPage(selenium);
            string googleSearchPhrase = "This is a test";
 
            // * Act
            googlePage.Search(googleSearchPhrase);
 
            // * Assert
            // 1. Should contain the search phrase
            WorkaroundFor(BOOGLE_01, 
                () => StringAssert.Contains(googleSearchPhrase, selenium.Title) // <==== тут
            );
 
            // 2. Should contain our Company name
            StringAssert.Contains("Google", selenium.Title);
        }


Не знаю, насколько Ваш подход будет полезен в модульных тестах, но вот в системно-интеграционных, аля Selenium через UI – будет очень полезен.
И вас с Новым годом, и вам спасибо :).

Забавно, насколько я понимаю, подход у вас получился похожий очень? Это интересно.

Почитал вашу статью, мы в подобных случаях для устранения эффекта разбитых окон такие тесты в CI-системе карантинили. В таком случае тест все еще выполняется, его результат логируется и доступен для просмотра, но падение теста в карантине не вызывает паденеия всего билда. Очень удобно получалось. Не знаю, есть ли у вас в системе подобная возможность. Мы используем Bamboo.
У нас проект просто такой, в который CI система никак не клеится. Сборка и тестирование билда у нас отдельно. Билд тестировать можно только из инсталляции. Ну, а релизы у нас раз в год. В итоге, авто-тесты нужны только тестировщикам :). Некоторые разработчики тоже пишут юнит и интеграционные тесты, но всё это децентрализированно, не для всей команды.
А билды собираются посредством JScript и bat-файлов.
Также (через bat-файлы) выглядит и последующий запуск тестов:

  1. Выкачивается новый билд,
  2. Готовится виртуалка
  3. На виртуалку копируются тесты.
  4. Запускаются тесты при помощи Gallio/MbUnit
  5. В последствии отправляется отчет по почте, который можно потом благодаря интеграции Gallio + Visual Studio на своей машине и проанализировать/пофиксить.


В такой системе нет карантина. И каждый упавший тест каждый раз отвлекает очень сильно. Так что их можно либо пофиксить либо подавить ассерты с известными багами.
Очень рекомендую попробовать MSpec. Скажем так, описаное в этой статье там доступно «из коробки» и необходимости докручивать это что-то к велосипеду нет :)
Посмотрел, спасибо. Там, получается, используется в точности такой же подход, с передачей делегатов. Только переходить с NUnit на другой фреймворк уже будет слишком радикальным шагом. Но вообще здорово, что в таком фреймворке реализовали настолько похожее решение.
В свое время писал что-то подобное. В итоге выложил вот сюда: Assertion.NUnit.
Код. Примеры использования. Из плюшек: сравнение «тестовых наборов» по содержимому и результатам. Написал эту штуку, когда необходимо было сравнить результаты выполнения тестов для небольшого количества комбинаций входных данных, но при большом количестве проверяемых условий для каждого тестового набора.
У вас более широкий по возможностям хелпер. Но в целом опять же получается, что решение по сути очень похоже, код использования моего аккумулятора вообще практически идентичен вашему коду с демонстрацией базовых возможностей на главной странице. Это радует :).
Sign up to leave a comment.

Articles