Несколько недель назад я прочитал Ценность открытого исходного кода — это видение, а не исходный код, и это заставило меня задуматься о своих собственных причинах запуска Fluent Assertions , теперь более года назад. В свете этой статьи, давайте кратко рассмотрим мои собственные цели для Свободных Утверждений.
Намерение модульного теста должно быть ясно из кода
Тест на выявление намерений имеет следующие характеристики
- Части аранжировки, действия и утверждения четко различимы
- Имя представляет функциональный сценарий, проверяемый тестом.
- Утверждение обеспечивает достаточный контекст, чтобы понять, почему ожидается конкретное значение
Например, рассмотрим этот небольшой тест.
[TestMethod] public void When_assigning_the_endorsing_authorities_it_should_update_the_property() { //----------------------------------------------------------------------------------------------------------- // Arrange //----------------------------------------------------------------------------------------------------------- Permit permit = new PermitBuilder().Build(); //----------------------------------------------------------------------------------------------------------- // Act //----------------------------------------------------------------------------------------------------------- permit.AssignEndorsingAuthorities(new[] { "john", "jane"}); //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- permit.EndorsingAuthorities.Should().BeEquivalentTo(new[] { "john", "jane"}); }
Да, я согласен, что это очень простой тест и этот сценарий должен быть протестирован как часть более широкой области. Тем не менее, это ясно иллюстрирует мои три пункта. Теперь рассмотрим этот тест.
[TestMethod] public void When_a_substance_is_specified_that_is_not_required_it_should_throw() { //----------------------------------------------------------------------------------------------------------- // Arrange //----------------------------------------------------------------------------------------------------------- var permit = new PermitBuilder().Build(); var someUser = new UserBuilder().Build(); var dataMapper = new InMemoryDataMapper(permit, someUser); var service = new CommandServiceBuilder().Using(dataMapper).Build(); //----------------------------------------------------------------------------------------------------------- // Act //----------------------------------------------------------------------------------------------------------- try { service.Execute(new AddMeasurementsCommand { Id = permit.Id, Version = permit.Version, Username = someUser.Username, Measurements = new[] { new MeasurementData("Oxygen", 1.1d, new DateTime(2011, 4, 13, 16, 30, 0)) }, }); Assert.Fail("The expected exception was not thrown"); } //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- catch (InvalidOperationException exc) { Assert.IsTrue(exc.Message.Contains("not required")); } }
Это тестирование конкретного бизнес-действия на соответствие одному из его бизнес-правил. Это довольно легко понять, но все еще много шума, вызванного конструкцией try … catch и дополнительным Assert.Fail (), чтобы утверждать, что исключение вообще было выдано . Менее важным, но все же немного неясным является использование конструктора DateTime . С Беглыми Утверждениями мы можем сделать лучше.
//----------------------------------------------------------------------------------------------------------- // Act //----------------------------------------------------------------------------------------------------------- Action action = () => service.Execute(new AddMeasurementsCommand { Id = permit.Id, Version = permit.Version, Username = user.Username, Measurements = new[] { new MeasurementData("Oxygen", 1.1d, 13.April(2011).At(16, 30)), }, }); //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- action .ShouldThrow<InvalidOperationException>() .WithMessage("not required", ComparisonMode.Substring);
Нечто подобное также возможно для проверки того, что события были вызваны, благодаря специальной поддержке интерфейса INotifyPropertyChanged, столь распространенного в проектах Silverlight и WPF. Я писал об этом ранее в этом году .
Проведенный модульный тест должен как можно лучше объяснить, что пошло не так
Нет ничего более раздражающего, чем модульное тестирование, которое не дает четкого объяснения причин. Чаще всего вам нужно установить точку останова и запустить отладчик, чтобы выяснить, что пошло не так. Джереми Д. Миллер однажды дал совет «держаться подальше от ада отладчика», и я могу только с этим согласиться.
Например, тестируйте только одно условие для каждого теста. Если вы этого не сделаете, а первое условие не выполнится, тестовый движок даже не попытается проверить другие условия. Но если кто-то из других потерпит неудачу, вы сами сможете выяснить, какой из них. Я часто сталкиваюсь с этой проблемой, когда разработчики пытаются объединить несколько связанных тестов, которые проверяют член, используя разные параметры, в одном тестовом примере. Если вам действительно нужно это сделать, рассмотрите возможность использования параметризованного теста, который вызывается несколькими четко названными тестовыми примерами.
Очевидно, я разработал Fluent Assertions, чтобы помочь вам в этой области. Не только с использованием четко названных методов подтверждения, но также и за счет того, что сообщение об ошибке предоставляет как можно больше информации. Рассмотрим этот пример:
"1234567890".Should().Be("0987654321");
Тот факт, что обе строки отображаются в отдельной строке, является намеренным и происходит, если длина любой из них превышает 8 символов. Однако, если этого недостаточно, все методы утверждений имеют необязательную форматированную причину с заполнителями, аналогично String.Format, которую можно использовать для обогащения сообщения об ошибке. Например, утверждение
new[] { 1, 2, 3 }.Should().Contain(item => item > 3, "at least {0} item should be larger than 3", 1);
потерпит неудачу с:
Сам код должен быть отличным примером того, как должен выглядеть высококачественный код
Эта цель должна быть достаточно очевидной. Я не только хочу создать отличную основу, но и хочу, чтобы люди учились у нее. И разве это не единственная главная причина, почему люди присоединяются к проектам с открытым исходным кодом? Тем не менее, в статье, которую я упоминал ранее, говорится, что видение проекта с открытым исходным кодом должно быть гораздо важнее, чем качество исходного кода. Как и следовало ожидать, я не согласен с этим. Фактически, одной из проблем, с которыми я столкнулся, было мое желание контролировать все вклады кода, чтобы они соответствовали Чистому коду , моим собственным рекомендациям по кодированию и моим идеям о модульном тестировании.
Сначала статья заставила меня усомниться в подходе, который я выбрал, но потом я решил, что качество кода столь же важно. Теперь я отвечаю на все запросы о вкладе с некоторой информацией о том, как я бы хотел, чтобы ФА развивалась. Кроме того, я создал специальную ветку для вкладов, которую они могут использовать. В зависимости от качества их вклада, объединение их в основную ветвь требует соответствующего объема работы на моей стороне. Я знаю, что это могло бы удержать некоторых участников, но до сих пор большинство из них согласились с моим подходом и были готовы предоставить высококачественный код.