Страницы

Поиск по вопросам

Показаны сообщения с ярлыком test-driven-development. Показать все сообщения
Показаны сообщения с ярлыком test-driven-development. Показать все сообщения

воскресенье, 8 марта 2020 г.

Java. TDD. Как проверить в assertThat() что объект имеет определенный тип (как instanceof)

#java #test_driven_development


Надо в тесте проверить что определенный объект имеет определенный тип.

boolean res = (desc[2][7] instanceof Place);
assertThat(res,is(true));


Что-бы вот так не писать... Есть ли какой-то метод в util?
    


Ответы

Ответ 1



Нужно использовать org.hamcrest.CoreMatchers.instanceOf Ответ: https://stackoverflow.com/a/12404813/4828657

Ответ 2



Пример: import org.junit.Test; import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertThat; abstract class AbstractClass { } class ClassImp extends AbstractClass { } public class InstanceOfTest { @Test public void testInstanceOf() { final AbstractClass subClass = new ClassImp(); assertThat(subClass, instanceOf(AbstractClass.class)); } }

понедельник, 24 февраля 2020 г.

Как покрыть тестами конструктор класса в java?

#java #intellij_idea #юнит_тесты #test_driven_development


Есть код, который по алгоритму Эвклида находит наибольший общий делитель. 

import java.util.Scanner;

/**
 * Created by user on 24.11.2015.
 * По данным двум числам 1 b) {
                return Euclid(a % b, b);          //рекурсивно вызовется             
            } //  алгоритм, если будет остаток от деления большего              
                                    
            if (b > a) {       //  числа на меньшее и наоборот    
                return Euclid(a, b % a); 
            } else return Euclid(a % b, b);

        }
    }
        public static void main(String[] args) {

        Scanner sc1 = new Scanner(System.in);      //ввод с клавиатуры 
        Scanner sc2 = new Scanner(System.in);
        int a = sc1.nextInt();
        int b = sc2.nextInt();
            System.out.println(Euclid(a,b));
    }
}


Несмотря на то, что программа работает, я решил по практиковаться на ней в разработке
через тестирование. 

Пишу тест: 

import org.junit.Test;

import static org.junit.Assert.*;
public class EuclidTest {

    @Test
    public void testEuclid() throws Exception {

        int result = new Euclid(234, 45); //в этой строке ошибка компиляции 
        assertEquals(9, result, 1e-9);
    }
}


В строке, где я объявляю result мне показывает ошибку компиляции.
 

Когда я переписал класс, заменив конструктор методом с другим названием всё прошло
как по маслу. Связи с этим вопросы:


Можно ли покрыть конструктор тестами так, чтобы не вызвать ошибку компиляции?
Как это сделать?

    


Ответы

Ответ 1



public static int Euclid(int a, int b) { //конструктор Дело в том, что это у вас не конструктор, а статический метод, возвращающий int. Конструктор был бы такой (он возвращает объект класса Euclid): public Euclid(int a, int b) { //конструктор Вообще, конструктор здесь не нужен никоим образом. Вам не нужно хранить какое-то состояние, так что и объекты создавать незачем. Переименуйте ваш метод euclid с маленькой буквы, как положено по стандарту именования и пишите вот такой тест: import org.junit.Test; import static org.junit.Assert.*; public class EuclidTest { @Test public void testEuclid() throws Exception { assertEquals(9, euclid(234, 45), 1e-9); } }

В каких случаях стоит, а в каких - нет, использовать TDD? Чем опасно его не использовать? [закрыт]

#ruby_on_rails #веб_программирование #test_driven_development


        
             
                
                    
                        
                            Закрыт. На этот вопрос невозможно дать объективный ответ.
Ответы на него в данный момент не принимаются.
                            
                        
                    
                
                            
                                
                
                        
                            
                        
                    
                        
                            Хотите улучшить этот вопрос? Переформулируйте вопрос,
чтобы на него можно было дать ответ, основанный на фактах и цитатах, отредактировав его.
                        
                        Закрыт 3 года назад.
                                                                                
           
                
        
Никак не могу понять, как TDD сумел обрести такую популярность. Почему? Потому что
я считаю это максимально иррациональным подходом к разработке:


Ты должен написать тест, который заведомо провалится.
Трата времени. Ты не написал ни одной строчки кода, зато написал 20 строк кода, чтобы
убедиться в том, что то, чего ты не написал, не работает.
Ты должен написать тест, который реализует твои мысли относительно того, как это
должно работать.
Трата времени. За то время, пока ты реализовывал свои мысли в строках теста, ты мог
реализовать свои мысли строками кода.
Ты должен написать код так, чтобы он прошел написанный тест.
А в это время ты уже мог заниматься рефакторингом написанного кода.
Рефакторинг.




Еще одна особенность. Как мы знаем, веб-разработка предполагает разработку того,
с чем конечный пользователь будет взаимодействовать через браузер. Подход TDD предполагает
максимальное абстрагирование от браузера и использование для проверки работоспособности
кода только командную строку. Чувствуете этот запах? Так пахнет логика.  



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

Изначально я знал, что люди с нелюбовью относятся к имеющемуся в rails стандартному
test-фреймворку. И я также прекрасно знал, что подавляющее большинство предпочитает
ему использование Rspec.

Первым делом я, конечно же, решил ознакомиться с документацией по Rspec. По первой
же ссылке в гугле я попал на эту страницу. Красивый landing-page с кучей видео разряда
ни о чем. Окей, почитаю документацию, решил я. Абсолютно ничего дельного эта страница
мне не принесла. Лишь внизу красовалась ссылка на какой-то Relish. Что это такое и
каким боком оно относится к Rspec - непонятно. Ладно, переходим.
Только оттуда с непримечательной ссылки Rspec-core (догадайся 2) я попал на нечто,
что хоть как-то похоже на то, что я ищу. Какие-то примеры кода. Да, сразу код. Не понятно,
как установить, какая структура должна быть у папок, как нужно именовать файлы, как
запускать - ничего.  

Спустя день, перелопатив кучу инфы по этому поводу, я узнал, куда стоит класть файлы,
как именовать папки, что require-ить, какие методы использовать (базу). Помогли мне
в этом довольно полезные скринкасты (на которых, кстати говоря, было полностью разработано
приложение без использования тестов, а тесты писались "в обучающих нас целях", когда
все уже заведомо работало так, как надо, и в данном случае тесты подгонялись под код,
а не наоборот (как это предполагает TDD), что еще раз доказывает нам ненадобность использования
оного). Не было ни какой конкретной инфы (в виде гайда, как это сделано в случае rails,
или хотя бы общей спарвки) ни на хабре, ни на railscasts (даже с pro-подпиской), на
которых R. Bates в своей привычной манере "проскакал" по верхам, показав типичный пример
из ряда "сделайте так-то, у меня получилось так-то, на это до свидания".

Садимся писать. Создаем приложение, устанавливаем необходимые гемы, пишем. Решил
я начать по-порядку. Роутинг. Написал тесты, которые должны были заведомо провалиться.
Провалились не тесты, а сам Rspec. Оказалось, ошибка синтаксиса (правильно, я же еще
должен знать, как писать). Тратим время на поиск примеров. Реализуем. Тесты провалились.
Реализовываем код (а что там реализовывать? Написал необходимые мне пути). Тесты прошли.
На все про все у меня ушло около 3-х часов. За это время я бы успел реализовать не
только логику routing'а, но и логику модели и отчасти каких-то контроллеров.  

Решил заняться написанием контроллеров. Опять уперся в банальное незнание и неумение
пользоваться новым инструментом. Как человек наученный каким-то опытом, не стал проклинать
в этом всех и вся, а просто полез в документацию. Окей, гугл, "rspec controller". Это
все, что нам предоставляет, насколько я понял, официальная документация. Опять же этот
Relish. На этом этапе я матюкнулся, послал все к чертям и решил немного отдохнуть.
Теперь, отдухнув, пишу сюда. И нет, пишу я не с целью выговориться или в очередной
раз, но уже на публику, проклясть этот TDD, а разобраться.  

Да, даже несмотря на все мои неудачи, я хочу разобраться, во-первых, в том, почему
же этот TDD так популярен? Почему все "настоятельно рекомендуют закрыть браузер и написать
парочку тестов", когда можно спокойно (и даже более полезно для себя с точки зрения
психики) обойтись простым визитом на localhost:3000? Что движет всеми теми, кто так
усердно (если такие вообще есть, и это не просто показуха) использует TDD? И самое
главное: в чем опасность отказа от этого подхода? Может ли отказ как-то сказаться на
твоем резюме или в поиске работы?  

Если есть действительно весомые аргументы против моей точки зрения, то прошу подкрепить
свой ответ ссылками на обучающие материалы по этой теме (желательно от и до, подкрепленные
real-life примерами и охватывающие хотя бы половину из того, с чем можно столкнуться
при реальном использовании).

Спасибо тем, кто осилил. Заранее извиняюсь, если это оскорбило чьи-то чувства. Предполагается,
что все, что написано выше - ИМХО.
    


Ответы

Ответ 1



Здесь есть три составляющие: Во-первых, сами тесты. Они нужны, в первую очередь, чтобы при дальнейшем изменении приложения не ломался старый функционал. Понятно, что, если приложение достаточно маленькое, быстрее и приятнее прокликать всё руками. Но с каждой новой фичой такое прокликивание будет занимать всё больше времени. К тому же, какие-то кейсы могут быть забыты. Автотесты же помогают прогнать тесты быстро и минимизировать человеческий фактор. В итоге, появляется возможность использовать CI. RSpec, в основном, подразумевает написание модульных тестов. Однако можно использовать, например Cucumber - тесты больше похожи на привычное "прокликать". Но, при этом, сами сценарии тестов становятся сложнее, т.к. сразу нужно проверить гораздо больше кейсов. Во-вторых, TDD. Пожалуй, в рамках TDD тесты понятнее будет называть спецификациями (specifications, specs - в RSpec). По сути, это текст задания, написанный в понятном для интерпретатора виде. На сколько написание спецификаций до кода себя оправдывает - один из холиваров. В любом случае, автотесты нужны. А в какой момент их писать - решать тебе. Лично мне было сложно писать "test first" (да и "last") до знакомства с принципами SOLID. Но, теперь, как сайд-эффект, мне легче проектировать архитектуру классов. Специфика RSspec такова, что легче тестировать солидные классы. (Спеки для несолидных получаются длинные, с кучей стабов и моков.). Судя по статье в вики, не я один отметил этот эффект: Разработка через тестирование предлагает больше, чем просто проверку корректности, она также влияет на дизайн программы. Разработка через тестирование способствует более модульному, гибкому и расширяемому коду. В-третьих, RSpec. Как я уже говорил, это не единственный фреймворк для написание тестов. На сколько я понял, основные проблемы возникли именно из-за его незнания. Но это не делает его "плохим". Писать быстро на незнакомом фреймворке вряд ли получится хоть у кого-то. Да, написание тестов, как и любого другого кода, занимает дополнительное время. Но обычно, всё же, это не 3 часа вместо 15 минут. В твоём же случае, это были затраты на обучение а не написание. Кстати, я в первый раз вижу, чтобы тестировали роуты. Обычно не требуется покрытие всего кода тестами. Корректность части компонентов (например тех же роутов) будет очевидна из корректности остальных компонентов. Ну и на практике, TDD хорошо подходит для крупных проектов, в которых проектирование на должном уровне. Для стартапов, функционал которых не очень велик, зато очень часты изменения - может быть излишним. Для лендингов, весь бэкенд сводится к отправки email с введённым пользователем телефоном - тоже.

Ответ 2



Надо понимать на что и как писать тесты, в каждом проекте может быть очень важный участок, внесение изменений в который очень критично - например модуль для подсчета денежной информации, так и участок менее важный - например ui админки сайта. А так, навскидку: Поддерживаемость - Тесты нужны, например, если вы собираетесь без боли рефакторить код, потом, через год, или не вы. Модульность, принцип единой ответственности - TDD вынуждает разработчика писать чище и проще, заранее придумывать апи, держать цикломатическую сложность в тонусе.

среда, 22 января 2020 г.

Выражение Assert при тестировании метода, который обновляет объект через сторонний сервис

#c_sharp #юнит_тесты #test_driven_development #mocking #nsubstitute


Код

public class Company
{
    public string Name { get; set; }
}

public class Customer
{
    public string Surname { get; set; }
    public string Name { get; set; }
    public Company Company { get; set; }
}

public class CustomerDto
{        
    public int Id { get; set; }

    [Required]
    public string Surname { get; set; }

    [Required]
    public string Name { get; set; }

    public string SecondName { get; set; }
}

public class CustomerService
{
    private readonly IGenericRepository _customerRepository;

    public CustomerService(IGenericRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }

    public CustomerDto Add(CustomerDto dto)
    {
        //Validation
        if (dto == null)
        {
            throw new ArgumentNullException(nameof(dto));
        }
        var validContext = new System.ComponentModel.DataAnnotations.ValidationContext(dto);
        Validator.ValidateObject(dto, validContext);

        //Mapping by AutoMapper
        Mapper.Initialize(cfg => cfg.CreateMap());
        var entity = Mapper.Map(dto);            

        _customerRepository.Insert(entity);
        _customerRepository.Save();

        dto.Id = entity.Id;

        return dto;
    }
}




Юнит-тестирование CustomerService.Add()

Необходимо создать юнит-тест для метода Add(). Изначально, вообще была попытка реализовать
этот метод через TDD, т.е. сначала создать для него юнит-тест и только затем имлемантацию.
Но я не смог этого сделать так как не разобрался за что конкретно отвечает этот метод
и надо ли вообще писать для него тест.

Но два юнит-теста я сделал и вот как все это выглядит:

[TestClass]
public class Customer_Test
{
    [TestMethod]
    public void Add_Test()
    {
        //Arrange
        //Use NSubstitude for mocking
        var mockRepository = Substitute.For>();

        //Act
        var service = new CustomerService(mockRepository);
        var dto = new CustomerDto
        {
            Surname = "Shakhabov",
            Name = "Adam"
        };
        var addedDto = service.Add(dto);

        //Assert
        Assert.IsNotNull(addedDto);
    }

    [TestMethod]
    [ExpectedException(typeof(ValidationException))]
    public void Add_TestException()
    {
        //Arrange
        var mockRepository = Substitute.For>();

        //Act
        var service = new CustomerService(mockRepository);
        var dto = new CustomerDto
        {
            Surname = "Shakhabov",                
        };
        var addedDto = service.Add(dto);

        //Assert
        //expectedException ValidationException
    }
}


Вот что (цель) должен сделать метод: Добавить в базу данных объект на основе полученного
DTO-объекта и затем обратно вернуть этот DTO-объект, но уже с обновленным полем Id,
значение для которого сформировала СУБД. 

Вот как (задачи) делает это метод:


Получив DTO-объект, сразу верифицирует его;
Используя библиотеку AutoMapper, проецирует DTO на сущность, которая в свою очередь
создается с нуля;
Используя объект-репозиторий, добавляет сущность в контекст.
Сохраняет контекст в результате чего источник данных (СУБД)
инициализирует поле Id у сущности (entity);
Инициализирует поле dto.Id полем entity.Id и возвращает DTO.




Вопрос

Правильно ли были сформулированы требования для метода Add() и соответственно построены
юнит-тесты на их основе? Какие тесты (выражения Assert) создаются для методов с подобной
логикой, где целью является инициализировать свойство DTO-объекта значением, получаемое
сторонним сервисом (в данном случаи объект-репозиторий)?
    


Ответы

Ответ 1



Смотрим в метод Add. Что делает метод добавления? Добавляет новую сущность в БД и присваивает ей уникальный идентификатор. С точки зрения возвращаемого значения - нужно вернуть только идентификатор, так как весь объект остался без изменений. Когда DTO приходит в метод добавления - идентификатор нулевой, то есть не заполнен. Когда сущьность (entity) будет добавлена в БД, ей будет присвоен новый уникальный идентифкатор. Так как используется тип данных int, то иднетификатор обязательно будет положительным (больше нуля) и уникален. Конвертацию из DTO в объект сущности БД перенести внутрь DTO или добавить конструктор в сущность БД, который примет в качестве параметра DTO-шку. Про конвертацию подробнее: либо в объекте Customer сделать конструктор, который будет принимать объект типа CustomerDto, где внутри произвести присвоение полей с помощью AutoMapper, либо в CustomerDto сделать метод ToCustomer(), внутри которого создастся объект Customer и будут присвоены поля, который вернет объект типа Customer. Думаю, что с точки зрения логики, метод Add не должен делать эту конвертацию, так как его задача взять объект, добавить его и на основании результата уже будем знать, прошло ли все успешно. Пример кода метода ToCustomer() в классе CustomerDto, для этого возможно понадобится подключить using для типа Customer и для AutoMapper, но это не беда, вот что я имел в виду: public Customer ToCustomer() { //Mapping by AutoMapper Mapper.Initialize(cfg => cfg.CreateMap()); var entity = Mapper.Map(dto); return entity; } По-хорошему и валидацию сделать бы более компактной и красивой, но это уже на ваше усмотрение. Еще один совет, инкапсулируйте в репозитрорий методы Insert и Save внутрь одного метода, который выполнит это (назовите его Create или Add). Ну и навесьте try...catch внутри метода, к примеру, если что-то пошло не так и упало исключение - тогда метод вернул -1, а в тестах вы уже будете знать, что если меньше нуля - то тест не прошел. Касаемо тестов, у вас два сценария: метод отработал успешно и без исключения - нужно проверить, что идентификатор не нулевой (больше нуля); пробуем подсунуть некорректные данные, которые вызовут исключение. Соответственно, тестовый метод, проверяющий успешную работу должен проверить, что идентификатор не нулевой. Пример проверки: C# unit test, how to test greater than Там где ожидается исключение - не должно быть Assert проверок. Касаемо TDD - это сложная практика, которая нарабатывается с опытом. Проще начать с написания простых классов через TDD, а потом уже переходить к более сложным сценариям. Чтобы писать через TDD, нужно иметь четкое представление того, как должна работать та или иная функциональность, которую вы хотите реализовать с ее помощью. А еще, сейчас я занимаюсь приведением большого проекта в порядок с помощью одного вспомогательного компонента под названием StyleCop, так вот он не рекомендует использовать знак нижнего подчеркивания в именовании свойств, полей, параметров, переменных и прочего. Ну это так, к слову. Надеюсь, что мои советы и пояснения достаточно ясны и не вызовут противоречий.

четверг, 9 января 2020 г.

Unit-тестирование работы с файловой системой

#юнит_тесты #тестирование #test_driven_development


Возможно ли unit-тестирование класса, основная задача которого состоит во взаимодействии
с файловой системой? Ну например, мне нужно протестировать класс, который предоставляет
возможность записывать/читать биты из файла. Каким образом я могу это сделать? Можно,
конечно, записать файл с какими-то тестовыми данными, а в самом тесте проверять что
считываются именно те данные. Мне это кажется каким-то костылем. Так ли это и какие
есть еще способы протестировать работу такого класса?
    


Ответы

Ответ 1



Давайте попробуем пройтись по задачке в стиле TDD: Какие публичные методы должен иметь данный класс? Пусть это будет только чтение Read(path, offset, count) и запись Write(name, offset, count). Пока мы не пишем их код, пока нам надо придумать, как бы мы их могли протестировать? Напишем несколько тестов (которые, кстати, помогут нам более точно описать поведение и интерфейс класса): Прочитать что-то из файла и проверить, что прочиталось именно то что надо. Что должно произойти, если отдана команда прочитать 100 байт из файла размером 50 байт? Вернуть 50 байт, вернуть ошибку/исключение, или вернуть 100 байт из которых последние 50 - пустые? Выбирайте вариант и пишите на него тест. Что должно произойти, если файл не существует или недоступен? Опять же, выбирайте "правильное" поведение и пишите на него тест. Разрешаем ли мы передавать отрицательное значение offset, и что оно будет означать (например чтение с конца, а не с начала). Пишем тест. Будем ли мы поддерживать чтение/запись по локальной сети? Длинные имена файлов (260+)? Запись в системные папки (с требованием админских прав)? Что еще должно работать и что может пойти не так .. продумайте желаемое поведение и напишите на него тесты. Повторите то же для метода записи в файл. Не стремитесь сразу предусмотреть ВСЁ. Учитывайте только то что вам понадобится в обозримом будущем (согласно вашему ТЗ). Теперь можно сделать паузу, и обобщить все те моменты и тонкости которые всплыли при проектировании тестов. Окинуть их взглядом еще раз, прикинуть какие ситуации требуют приватных методов и функций, общих для чтения и записи (например, проверка существования файла, или размера). А вот только теперь можно приступать к написанию кода самого класса чтения/записи в файл! Встретили какое-то еще условие, или граничный случай в процессе разработки - отлично, напишите на него тест, а потом код. В процессе эксплуатации класса выявился баг - тоже отлично, пишите на него тест, и только потом исправляйте баг (и тестом проверяйте, что он исправлен, и что в процессе исправления ничего из другого не сломалось).

понедельник, 6 января 2020 г.

Объясните принцип разработки TDD

#c_sharp #юнит_тесты #test_driven_development


Нашел такое задание в курсе по C#:

Implement Vending machine in code using TDD approach

There are following features:


You can update product list at any time
You can insert coins, get coins back and get remainder
You can buy 1 product at once for inserted coins
Machine accepts following coins: 5ȼ, 10ȼ, 20ȼ, 50ȼ, 1 € and 2 €


Есть такой интерфейс:

  public interface IVendingMachine
  {
    /// Vending machine manufacturer. 
    string Manufacturer { get; }

    /// Amount of money inserted into vending machine.  
    Money Amount { get; }

    /// Products that are sold. 
    Product[] Products { get; set; }

    /// Inserts the coin into vending machine. 
    /// Coin amount.
    Money InsertCoin(Money amount);

    /// Returns all inserted coins back to user. 
    Money ReturnMoney();

    /// Buys product from list of product. 
    /// Product number in vending machine product list.
    Product Buy(int productNumber);
  }

  public struct Money
  {
    public int Euros { get; set; }
    public int Cents { get; set; }
  }

  public struct Product
  {
    /// Gets orsets the available amount of product. 
    public int Available { get; set; }

    /// Gets orsetsthe product price. 
    public Money Price { get; set; }

    /// Gets orsetsthe product name. 
    public string Name { get; set; }
  }


Как будет выглядеть разработка от тестов?
Насколько я понял сначала нужно реализовать тест метода а потом его написать, но
не понимаю как можно написать сначала юнит-тест, а потом реализовать метод.
Раньше не писал никогда юниты, можете помочь объяснить и показать на примере?
    


Ответы

Ответ 1



Хорошее задание. Главное - у вас есть предметная область, а именно - то что надо уметь выполнять (требования и возможности сущности). В вашем случае - начинаем с создания класса, реализующего интерфейс. Пока пустого: public class VendingMachine : IVendingMachine { public Money Amount { get; } public Product[] Products { get; set; } public Money InsertCoin(Money amount) { throw new NotImplementedException(); } public Money ReturnMoney() { throw new NotImplementedException(); } public Product Buy(int productNumber) { throw new NotImplementedException(); } public string Manufacturer { get; } } Первое требование - You can update product list at any time Интерфейс не имплементирует, как именно это будет происходить, а потому главный вопрос к TDD - как именно стороннему разработчику будет удобно обновлять ассортимент. Пишем тест на это дело: var machine = new VendingMachine(); machine.AddProduct(); Сразу задумываемся, а как же удобно рулить продуктами, ведь судя по структуре продукта, в ней же задается количество\название и цена экземпляра. Я предпочту простой вариант: var beerPrice = new Money() {Euros = 1, Cents = 10 }; machine.AddOrUpdateProduct("Пиво", beerPrice, 3); Сразу добавим метод с такой сигнатурой в класс: public void AddOrUpdateProduct(string name, Money beerPrice, int count) { throw new NotImplementedException(); } Теперь дописываем тест для того, чтобы он проверял необходимый нам кейс: var machine = new VendingMachine(); var beerPrice = new Money() {Euros = 1, Cents = 10 }; var count = 3; machine.AddOrUpdateProduct("Пиво", beerPrice, count); Assert.AreEqual(machine.Products.Length, 1); Assert.AreEqual(machine.Products[0].Available, count); Запускаем тест - получаем ошибку: Метод проверки TDD.UnitTest1.UpdateVendingMachineProducts выдал исключение: System.NotImplementedException: Метод или операция не реализована.. Сложная часть работы завершена, осталось реализовать простой способ, который поднимет тест. Простой, но действующий так, будто сущность существует в реальном мире, не стоит городить магию. Работать с массивами я лично не люблю, поэтому спрячу в реализации список, в итоге: public Product[] Products { get { return products.ToArray(); } set { products = value.ToList(); } } private List products = new List(); public void AddOrUpdateProduct(string name, Money beerPrice, int count) { products.Add(new Product() {Available = count, Name = name, Price = beerPrice}); } Тест успешно прошел, первая фича реализована, ура. Вторая - You can insert coins, get coins back and get remainder Описание дает элементарный намек на три теста - вставил денег, вернул, получил сдачу. Первые два реализовать можно прямо сейчас, третий - лучше отложить, сдача может быть только от покупки, а про неё пока ни слова. Тут нам интерфейс задан вполне неплохо, пишем тесты по нему: [TestMethod] public void InsertCoin() { var machine = new VendingMachine(); var inserted = new Money() {Euros = 1}; var returned = machine.InsertCoin(inserted); Assert.AreEqual(machine.Amount, inserted); Assert.AreEqual(returned, inserted); } [TestMethod] public void ReturnMoney() { var machine = new VendingMachine(); var count = 1; machine.InsertCoin(new Money() { Euros = count }); var back = machine.ReturnMoney(); Assert.AreEqual(back.Euros, count); Assert.AreEqual(machine.Amount.Euros, 0); } Реализуем: Деньги перегрузим, пусть сами складываются. Пока элементарно: public static Money operator +(Money m1, Money m2) { return new Money() { Euros = m1.Euros + m2.Euros, Cents = m1.Cents + m2.Cents }; } Итого, для первого теста: public Money Amount { get; protected set; } public Money InsertCoin(Money amount) { Amount = Amount + amount; return Amount; } И для второго: public Money ReturnMoney() { var amount = Amount; Amount = new Money(); return amount; } Не забываем про предметную часть - сумма в автомате должна меняться при этих операциях. Ура, у нас три зеленых теста. Третий случай - You can buy 1 product at once for inserted coins Если я правильно понял, можно купить только один продукт за раз. После этого сдача вернётся. Не пользовался автоматами, если ошибся - ну извините. Тест меня сразу обломал - интерфейс ввода для пользователя - цифровой! Будем считать, что цифры от 1. Пишем тест: [TestMethod] public void Buy() { var machine = new VendingMachine(); var beerPrice = new Money() { Euros = 1, Cents = 10 }; var count = 3; var name = "Пиво"; machine.InsertCoin(new Money() { Euros = count }); machine.AddOrUpdateProduct(name, beerPrice, count); var product = machine.Buy(1); Assert.AreEqual(machine.Products[0].Available, count - 1); Assert.AreEqual(product.Name, name); Assert.AreEqual(machine.Amount, default(Money)); } Фух, снаружи простой тест вызывает кучу вопросов. Деньги проще сделать хотя бы частично работающими с простыми операциями: public static Money operator +(Money m1, Money m2) { return new Money() { Euros = m1.Euros + m2.Euros, Cents = m1.Cents + m2.Cents }; } public static Money operator -(Money m1, Money m2) { return new Money() { Euros = m1.Euros - m2.Euros, Cents = m1.Cents - m2.Cents }; } public static bool operator <(Money m1, Money m2) { if (m1.Euros != m2.Euros) return m1.Euros < m2.Euros; return m1.Cents < m2.Cents; } public static bool operator >(Money m1, Money m2) { return m2 < m1; } public static bool operator ==(Money m1, Money m2) { return m1.Euros == m2.Euros && m1.Cents == m2.Cents; } public static bool operator !=(Money m1, Money m2) { return !(m1 == m2); } Продукту это тоже не помешает: public static Product operator +(Product m1, int m2) { return new Product() { Available = m1.Available + m2, Price = m1.Price, Name = m1.Name }; } public static Product operator -(Product m1, int m2) { return new Product() { Available = m1.Available - m2, Price = m1.Price, Name = m1.Name }; } Тогда, покупка выглядит более-менее простой, я правда так и не понял, какой товар вернуть должен интерфейс - я возвращаю остаток в автомате, чтобы его можно было отобразить например. public Product Buy(int productNumber) { if (products.Count < productNumber || productNumber < 1) throw new IndexOutOfRangeException("Товара нет."); var index = productNumber - 1; var product = products[index]; if (product.Available <= 0) throw new IndexOutOfRangeException("Товара нет."); if (Amount < product.Price) throw new Exception("Не хватает денег."); Amount = Amount - product.Price; ReturnMoney(); product = product - 1; products[index] = product; return product; } Итак, третий тест теперь тоже зеленый. Если вы думаете, что код пишется легко - я вас уверяю, быстрый запуск теста позволяет писать его ещё легче. Все исключения в методе Buy написаны исключительно благодаря тесту, но даже так какие то кейсы я мог пропустить. Я не буду описывать последний кейс - в нём вся соль тестов. Надо написать тест, добавить ограничения и словить боль от падения предыдущих тестов, которые работали с покупкой и кривыми введенными данными. В этом вся соль TDD - вы написали себе логику прикладного разработчика, потом реализовали какую то внутреннюю и сложную хрень, а потом получили ту самую боль, которую испытывают все, кто пользуется вашей разработкой. Ваши страдания намного дешевле страданий пользователей, а потому TDD(который и PainDD вполне) и помогает писать тесты и стабильный продукт. Запускайте тест чаще при разработке - он сразу скажет, что вы забыли. При разработке методов - не забывайте проверять входные данные и данные окружающего мира, а то в тестах всегда есть желание дать невалидные данные и получить результат. Купить товар, не введя денег, запросить деньги, не добавляя их и прочее прочее. Чем больше у вас реальных кейсов поведения - тем конкретнее и полезнее будут ваши тесты, не стоит придумывать ерунды на тестирование. UPD: куча вещей пропущена и по причине того, что не нужны для тестов и по причине лени. Центы не складываются в евро, товар можно только добавить, хотя на деле нужно бы количественное добавление, цену товара нельзя поменять. Над чем то надо думать, что то уточнять у бизнес-аналитика или специалиста по предметной области - сложностей хватает.

Ответ 2



Понимание того что хотите создать. В общих чертах это представить Написать тест, как будто объект тестирования уже написан Запустить тест. Убедиться что тест упал. Это важно! Написать код в объекте тестирования код достаточный, чтобы скомпилировалось Запустить тест. Убедиться что тест пройден. Это важно! Написать код в объекте тестирования Запустить тест. Убедиться что тест пройден. Это важно! Провести рефакторинг, если нужно Запустить тест. Убедиться что тест пройден. Это важно! Нельзя пренебрегать запуском теста между этами разработки. Тест обязан падать там, где нужно упасть. Именно по этой причине п.3, п.5, п.7 и п.9 крайне важны! После того как объект тестирования реализован нужно убедиться, что тест: Находится в правильном наборе тестирования. Если это не так, то перенести куда следует; Содержит только и только одну проверку. Должна быть только и только одна причина по которой тест должен упасть. Другими словами должен быть только и только один assert. Если не получается, то воспользуйтесь библиотекой типа hamcrest; Выполняется достаточно быстро. Основное качество модульного теста это скорость работы. Если модульные тесты будут работать медленно, разработчик будет избегать их запуска.

Ответ 3



Насколько я понял сначала нужно реализовать тест метода а потом его написать Абсолютно верно. В этом и заключается основная идея разработки через тестирования (TDD). Определив интерфейс функции или класса, далее ты определяешь набор требований к функционалу через соответствие входных и выходных данных. Например тебе нужно реализовать поиск большего из 2х чисел (пример на С++, но смысл должен быть понятен) template T max(const T& a, const T& b); Условный набор тестов: assert(max(1, 4) == 4); assert(max(4, 1) == 4); assert(max(-1, -4) == -1); После этого ты начинаешь реализовывать метод и в конечном счете все тесты должны пройти. Аналогично с классом терминала продаж определив интерфейс пишешь тесты на то, как изменится внутреннее представление экземпляра класса при вызове его методов с разными параметрами

Ответ 4



не понимаю как можно написать сначала юнит-тест, а потом реализовать метод Это нормально. Этого никто не понимает. Не зная предметную область, не обсудив ТЗ с заказчиком и с коллегами, не имея большого опыта разработки в целом и опыта разработки в данной предметной области в частности, невозможно сразу начать писать тесты. Те, кто уверяют, что нужно сразу писать красный тест, слегка лукавят. Сколько я ни общался с такими разработчиками, сколько ни видел примеров в интернете, там всегда пишется тест на уже знакомую тему. То есть данный разработчик участвовал по крайней мере в одном проекте (а скорее в нескольких на протяжении ряда лет) в данной предметной области. И вот на основании своего предыдущего многолетнего опыта он и способен написать сперва тест. Но на самом деле, сперва был написан код: в предыдущих проектах. А как дело касается новой предметной области, опять начинается написание сперва пробных кусков кода, их переписывание, выбрасывание... Писать тест на код, который гарантированно будет выброшен, нет смысла. И лишь после нескольких итераций выкристаллизовывается некий каркас, по которому можно начинать писать тесты. Поэтому, не расстраивайтесь, когда у вас не будет сразу получаться писать сперва тесты и лишь потом код.

вторник, 31 декабря 2019 г.

Как протестировать junit вывод в консоль?

#java #юнит_тесты #test_driven_development


> public void add(Item item) {

>     if (!item.header.equals(null)) {

>         this.items[this.index] = item;

>         this.index++;

>     } else {

>         System.out.println("Please header enter.");

>     } }


хочу добавить тест на


} else {

    System.out.println("Please header enter.");



используя конструкцию


  assertThat(???, is(???));


Но что сюда подставлять если у меня консольное приложение? А метод void?
    


Ответы

Ответ 1



Не завязывайте приложение на логирование в консоль. Используйте отдельный объект-логгер, которому будуте передавать строку и уровень логирования. Это стандарт, так делается во множестве крупных проектов. Приложения работают на серверах, там некому пялиться в монитор и читать логи, поэтому сообщения пишутся туда, куда удобно разработчикам и админам — в файл, в сокет, отправляются по http, что угодно ещё. Могут и в stdout, как в вашем случае, но должен быть выбор. И если в stdout, то System.out.println находится в коде логгера, а не в том месте, где происходит логирование. И когда вам нужно будет протестировать логгер, вы просто подсунете вместо него тестовый объект, в котором будете проверять, что такой-то метод с такой-то строкой был вызван. Уже есть множество написанных логгеров, я когда-то использовал log4j, но не могу сравнить с другими.

пятница, 27 декабря 2019 г.

как автоматически создавать TestSuite?

#python #юнит_тесты #test_driven_development


Наверное тяжело будет тем кто не читал TDD Кента Бека, но думаю многие читали, поэтому
прошу помощи. Уже 2 дня бьюсь, с ног сбился. В конце части 2 там получилась вот такая
архитектура:

    class TestCase:
     def __init__(self, name):
        self.name = name
     def setUp(self):
        self.result = TestResult()
     def run(self, result):
        result.testStarted()
        self.setUp()
        try:
           method = getattr(self, self.name)
           method()
        except:
           result.testFailed()
        self.tearDown()
     def tearDown(self):
        pass

class WasRun(TestCase):
    def __init__(self, name):
        TestCase.__init__(self, name)
    def testMethod(self):
        self.wasRun=1
        self.log = self.log+"testMethod "
    def setUp(self):
        self.wasRun = None
        self.log = "setUp "
    def tearDown(self):
        self.log = self.log + "tearDown "
    def testBrokenMethod(self):
        raise Exception

class TestResult:
    def __init__(self):
        self.runCount = 0
        self.errorCount = 0
    def testStarted(self):
        self.runCount = self.runCount + 1
    def testFailed(self):
        self.errorCount = self.errorCount + 1
    def summary(self):
        return "%d run, %d failed" % (self.runCount, self.errorCount)

class TestSuite:
    def __init__(self):
        self.tests = []
    def add(self, test):
        self.tests.append(test)
    def run(self, result):
        for test in self.tests:
           test.run(result)       

class TestCaseTest(TestCase):
    def testTemplateMethod(self):
        test = WasRun("testMethod")
        test.run(self.result)
        assert("setUp testMethod tearDown " == test.log)
    def testResult(self):
        test = WasRun("testMethod")
        result = test.run(self.result)
        assert("1 run, 0 failed" == self.result.summary())
    def testFailedResult(self):
        test = WasRun("testBrokenMethod")
        result = test.run(self.result)
        assert("1 run, 1 failed" == self.result.summary())
    def testFailedResultFormatting(self):
        result = TestResult()
        result.testStarted()
        result.testFailed()
        assert("1 run, 1 failed" == self.result.summary())
    def testSuite(self):
        suite = TestSuite()
        suite.add(WasRun("testMethod"))
        suite.add(WasRun("testBrokenMethod"))
        result = TestResult()
        suite.run(self.result)
        assert("2 run, 1 failed" == self.result.summary())


Далее мы создаем тестовый набор, вот так:

suite = TestSuite()
result = TestResult()
suite.add(TestCaseTest("testTemplateMethod"))
suite.add(TestCaseTest("testResult"))
suite.add(TestCaseTest("testFailedResult"))
suite.add(TestCaseTest("testFailedResultFormatting"))
suite.add(TestCaseTest("testSuite"))
result = TestResult()
suite.run(result)
print result.summary()


И дальше Кент Бек пишет "Создать объект TestSuite автоматически на основе TestCase"

Не совсем понимаю, что это значит, подскажите пожалуйста. Я так понял: вместо тех
10 строк что я привел во втором листинге, тест должен запускаться одной:

TestCaseTest().run()


И после этой строки все тестовые методы, которые есть в тестовом классе должны выполняться.
Для этого нам нужен конструктор без параметров и метода run() без параметров. Но
у нас уже есть конструктор и метод run() с параметрами, то есть нужна перегрузка, но
перегрузки нет в Python. Значит нужно создать версии этих методов с параметрами по
умолчанию. Хотя бы правильная идея?
    


Ответы

Ответ 1



Оригинальная цитата из книги: There is substantial duplication here, which we could eliminate if we had a way of constructing a suite automatically given a test class. Да, под here понимаются как раз строчки suite.add(...). Согласитесь - ручками каждый метод добавлять не самая удачная идея, может быть их там 100. Т.е. подразумевается что хотелось бы реализовать функционал, который соберет TestSuite из методов TestCaseTest автоматически сам, без нашего участия. Реализовать это можно различными способами, думаю самый простой: С помощью dir() получить методы TestCaseTest и отфильтровать те из них, что начинаются с test* Создать TestSuite Пройтись в цикле по списку методов из пункта 1 и добавить их в TestSuite при помощи suite.add(TestCaseTest("имя_метода")) Теперь уже выполнить suite.run(result) на собранном TestSuite Облечь эту логику можно, например, в отдельный класс TestLoader. Вот на github товарищ решал эту задачу: https://github.com/lenin/TDDbyExample/blob/7275d259132b44a5f24b116b4f61e47e421d7d35/PyTesting/tests.py и https://github.com/lenin/TDDbyExample/blob/7275d259132b44a5f24b116b4f61e47e421d7d35/PyTesting/testing.py получилось довольно аккуратно на мой взгляд. P.S.: Интересная книжка, спасибо за наводку ;)

Ответ 2



Если Ваша цель упростить запуск тестов, то на этот случай моя рекомендация поставить пакет: pip install nose и использовать команду запуска: nosetests -v.

воскресенье, 24 ноября 2019 г.

Как правильно выбрать название для юнит-теста?


Это был обычный будний день, я начал писать очередной обычный тест, но при написании названия теста что-то пошло не так:

testCustomerRedirectUrlAfterSelectLastTransactionProcessShouldContainsUrlToConfirmRecurringProfilePageWhenTransactionTypeIsCreateRecurringProfile


Примерно на середине я начал осознавать что происходит что-то не то, но я не остановилс
и дописал название теста до конца. Коллеги не оценили такое длиннющее название теста, но мне оно нравится. Сейчас используется что-то вроде testCorrectChangeCustomerRedirectUrl, а детали того что тест тестирует скрыты внутри теста.

Я не могу понять это лвлап, либо я устал под вечер.

Вопрос: Каковы критерии выбора хорошего названия теста?

UPD: Пожалуйста, приведите названия тестов из ваших проектов.
    


Ответы

Ответ 1



Название для тестов не надо выбирать красивое. Название нужно выбирать функционально и по возможности короткое -- чтобы по минимальному количеству символов было понятно, что и под какими условиями он тестирует, а также чтобы можно было отличить конкретный тест от тысяч других. Распространенные шаблоны наименования такие: TestClass_TestMethod_ConditionAndExpectedResult (для юнит-тестов) TestMethod_Condition_ExpectedResult (для юнит-тестов) ConditionAndExpectedResult (для интеграционных тестов, которые тестируют целые блоки функциональности через некоторую входную точку) При этом если у вас в частях Condition/ExpectedResult слишком много разных условий/результатов объединенных по "И", то вам нужно пересмотреть либо главную цель теста (что он тестирует), либо главное условие, которое отличает тест от других, и разбить его на несколько отдельных тестов. Я ратую прежде всего за то, что название должно быть информативным, но при этом настольк коротким, насколько возможно. Тут точно так же как и со всем остальным кодом -- че меньше кода, тем меньше времени нужно чтобы его понять, тем проще разбираться с ним Судя по вашему названию, это не простой маленький юнит-тест, а что-то побольше, поэтому совсем коротким названием тут не обойтись. В вашем случае, если тест класс у вам уже для Customer, я бы ограничился названием SelectLastTransactionProcess_CreateRecurringProfile_RedirectUrlShouldContainConfirmation. В вашем названии много повторений слов: transaction, url, recurring profile и т.д. Они избыточны. Между длиной и информативностью названия безусловно должен быть баланс. Достаточно чтоб названия были понятны разработчикам внутри проекта, которые уже обладают контексто и способны понять, о чем этот тест. И если для этого нужно длину названия сделать н 30 символов, а 40, сделайте. Но не нужно стремиться к тому, чтоб названия были понятн каждому встречному, попутно рассказывая обо всей системе, поскольку это приводит к таким вот километровым названиям, в которых сложно разобраться. Посмотрите на свой обычный код, посмотрите на названия методов. Вы ведь не описываете в названии метода каждую его деталь, каждую строчку? Вы описываете нечто главное, а остальное спрятано внутри. Точно так же и с тестовыми методами. Кто-нибудь может ещё рассказать что делать с названиями интеграционных тестов > где 100500 различных условий? К примеру биллинг, принимает на вход 5-10 видов прайс-листов и в зависимости от их комбинаций и комбинаций их параметров (тоже примерно 5-10) должен выдавать различные суммы на выход. Контейнер отправляемый в биллинг занимает ~30 строчек. Если в стиле поста называть тесты то это будет testPayoutDebtorToGateOntopGateTransactionAmountGateToCreditorFlatPayinDebtorTo‌​GateOntopTotalTransactionAmountGateToCreditorOntopTransactionAmount, причем не с полным контекстом. Нужно рассчитать итоговую сумму исходя из того что прайс-листы могли быть настроены следующим образом: Тип комиссии прайс-листа типа основной сделки - inside/total, комиссия 0.1% + 0.5руб cверху, расчет вести от оборота. Второй комиссионный прайс-лист ontop/gate.. ещё в таком же стиле раз 10 и пара неявных зависимостей прайс-листов друг от друга. Я бы сказал, что вам нужен параметрический тест. Это такой тест, который одним кодо тестирует разные наборы данных. Например, в терминах тестового фреймворка xUnit параметрический тест метода Calculator.Add() может выглядеть так: [Theory] // это параметрический тест [InlineDate(1, 2, 3)] // наборы данных, т.е. параметры теста [InlineDate(0, 1, 1)] [InlineDate(-1, 1, 0)] public void Calculator_Add_ShouldReturnCorrectResult(int a, int b, int expectedResult) { var result = Calculator.Add(a, b); Assert.AreEqual(result, expectedResult); } С параметрическими тестами вам не нужно перечислять все условия в названиях -- названи содержит только суть теста, что именно тестируется, конкретный кейс. В вашем случа это что-то вроде РасчетИтоговойСтоимости_ПоОсновнойИДополнительнойСделке (на английский сами переведете, вам область понятнее). А выглядеть ваш тестовый метод может так (при этом в xUnit тестовые данные можно подставлять из свойства и даже из отдельного объекта): [Theory] [PropertyData("PriceData")] // тестовые данные находятся в отдельном свойстве public void РасчетИтоговойСтоимости_ПоОсновнойИДополнительнойСделке( ComissionType mainComissionType, Comission mainComission, CalculationType mainCalculationType, ComissionType auxComissionType, Comission auxComission, CalculationType auxCalculationType, double expectedPrice) { // код теста } public static IEnumerable PriceData { get { // данные можете захардкодить, а можете прочитать из файла return new[] { new object[] { // основная сделка ComissionType.InsideTotal, new Comission(0.1, 0.5), CalculationType.ОтОборота, // дополнительная сделка ComissionType.OnTopGate, new Comission(0.42, 0.99), CalculationType.ОтБалды, // результат 100500 }, // и так далее для других пар }; } } Я думаю во многих тестовых фреймворка есть возможность создания параметрических тестов Способ задания параметров может отличаться, но суть останется та же: данные выносятся в секцию с данным, в названии теста остается только название кейса. Дополнительную информацию о параметрических тестах можной найти в блоге Сергея Теплякова.

Ответ 2



Перед самым первым "модульным тестом" Нужно договориться с самим собою или командой о том, что считать "модульным тестом"? К примеру в книге Роя(см. в раздел "рекомендуемая литература") дается ясное определение "unit" и достаточно удобное для программиста. Без четкого и уверенного понимания, что такое "unit" писать модульные тесты настоятельн не рекомендую. Даже если Ваше понимание, что такое модульный тест отличаются от того что говорит Рой все равно оно должно быть унифицированным и стабильным для ВАС или Вашей команды. Что требует некоторой договоренности. Понимание "Модульный тест" от Роя: Рой, на мой взгляд, предлагает самую удобную и практичную схему именования: {unit-of-work} _ {scenario} _ {expected-results-or-behaviour} Где: unit-of-work - это как раз то что Вы понимаете под 'unit' или о чем договорилис с командой. scenario - это конкретное действие над тестируемым объектом expected-results-or-behaviour - это Ваши ожидания относительно того, что должно произойти Примеры: jsonParse_InvalidFileExtension_ThrowInvalidLogFileException Сразу видно, что проверяется парсер json-документа на файле с недопустимым файловым расширением и поэтому должно быть брошено InvalidLogFileException. opimizeByteCode_UnknownInstruction_ReturnMinusOne Тут видно, что при оптимизации байт-кода будет возвращаться -1 в случае если встретится неизвестная инструкция Написание модульных тестов со сложными условиями Это совершенно другой вопрос Если кратко, то в одном модульном тесте ОДНА проверка. Если условие сложно, значи там несколько под условий и значит надо Взять и разбить на несколько проверок - модульных тестов В любом тесте не должно быть условной логики. Если в production-коде есть две ветки, одна if-часть, другая else-часть. Значит пишется ДВА модульных теста. и т.д. и т.п. Но все это тема другого обсуждения Рекомендации по названиям тестов автора вопроса testCustomerRedirectUrlAfterSelectLastTransactionProcessShouldContainsUrlToConfirmRecurringProfilePageWhenTransactionTypeIsCreateRecurringProfile Что неправильно, на мой взгляд, в вашем тесте: Префикс 'test'. Если смотрю тестовый проект с набором модульных тестов и когда вижу что-то, то что это если не тест? В C# есть [Test], в python-е есть py.test. Этого достаточно! Имя не разделено на логические части. Где действие? Где условие? Где ожидаемый результат А где то что мы тестируем? Все смешалось в одну кучу! Любой программер либо поленится прочитать такое имя, либо начнет разделять на части и не факт, что он разделит его так, как делил на части автор теста! Слово 'when' оно лишнее! Если у Вас есть принятая схема именования, то Вы или Ваш коллега итак знает, где искать условие или действие! Рекомендуемая литература: Настоятельно рекомендую скачать, а лучше купить книгу Art Of Unit Testing. Даже если Вдруг не пишите на C#. Знания полезны! Ответ SO-участника @Timofey Bondarev на мой вопрос Как заменить префикс 'test_' на 'should_' в модульных тестах с примением unittest?

Ответ 3



1. Имена тестов должны быть строками Начать надо с того, что имя теста - это имя функции. В отличие от функций, мы н вызываем тесты из кода, имя теста нужно только для того чтобы один раз описать что он тестирует, чтобы это описание потом записалось в лог с результатами тестирования. По этому в некоторых тестовых фреймворках имена тестов - это обычные строки. Например в Catch (C++) TEST_CASE("что-то когда такие-то условия") { auto testee = Testee(1, 2); REQUIRE(testee.foo() == 1); } Или Mocha (JS и прочие *script) describe('что-то', function () { it('когда такие-то условия', function () { var testee = Testee(1, 2); assert.equal(testee.foo(), 1); }); }); Интересным исключением являются языки где идентификаторы могут иметь любые символы, как например в F# [] let ``twice удваивает числа`` () = twice(1) |> should equal 2 2. Не надо повторяться (принцип DRY) Если тестовый фреймворк позволяет группировать тесты, то это надо использовать. Вмест тестов Чтото_КогдаЭто_ВотЭто, Чтото_КогдаДругое_ВотЭто и Чтото_КогдаДругое_ЕщеИЭто тесты можно сгруппировать. Пример на Mocha/JS: describe('что-то', function () { var testee = Testee(); describe('когда так-то', function () { it('вот это', function () { testee.setup(1); assert.equal(testee.f(), 1); }); }); describe('когда по другому', function () { testee.setup(2); it('вот это', function () { assert.equal(testee.f(), 20); }); it('и еще это', function () { assert.equal(testee.g(), 21); }); }); }); Или для Catch/C++: TEST_CASE("что-то") { auto testee = Testee(); WHEN("когда так-то то вот это") { testee.setup(1); REQUIRE(testee.f() == 1); } WHEN("когда по другому") { testee.setup(2); THEN("вот это") { REQUIRE(testee.f() == 20); } THEN("и еще это") { REQUIRE(testee.g() == 21); } } } Также зачастую нет смысла повторять код теста в названии теста. Надо описывать чт проверяет тест, а не как он это делает. По этому вместо StartButton_WhenPressed_BecomesDisabled можно написать TEST_F(ButtonsPanelTest, StartButtonPress) { EXPECT_CALL(delegate_mock, Start()); start_button.Press(); ASSERT_FALSE(start_button.IsEnabled()); } Как правило тест включает в себя начальные условия (GIVEN/WHEN часть) и результат (THEN часть). Если для каких-то начальных условий тест один, то не зачем писать в названии то что проверяется в THEN части. Это видно из кода, и если тест провалится, то текст проваленной проверки будет написан в логе. Примеры имен тестов Имена тестов зависят от тестового фреймворка и от уровня теста (юнит/интеграционный/регрессионный). Регрессионные тесты на Mocha/JS describe("File IO", function() { it("can read text", function() {...}) it("invalid file name error", function() {...}) it("read after EOF error", function() {...}) }) Юнит-тесты на Catch/С++, когда в Catch использовались имена тестов разделенные / TEST_CASE_METHOD(ParserFixture, "Notifications/Empty") { SECTION("no 'type' field") {...} SECTION("empty updates") {...} } TEST_CASE_METHOD(ParserFixture, "Notifications/Status update") {...} TEST_CASE_METHOD(ParserFixture, "Notifications/Profile update") {...} Более новые тесты Catch/C++, с тегами TEST_CASE("request line", "[http][parser]") {...} TEST_CASE_METHOD(FrameReceiverFixture, "is frame complete", "[websocket]") {...} Тесты на gtest/C++ (из кода Chromium) TEST_F(DownloadItemTest, CompleteDelegate_SetDanger) {...} TEST_F(DownloadItemTest, NotificationAfterOnDownloadTargetDetermined) {...} Тесты на Golang func TestSomeClassMethod_CanDoThis(t *testing.T) {...} Если имя теста нельзя задать строкой, то одновременное использование CamelCase нижнего подчеркивания дает довольно неплохо повышает читабельность. Например вместо TEST_CASE("some unit") { WHEN("this state") { THEN("those results") { ... можно написать SomeUnit_WhenThisState_ThoseResults.

Ответ 4



Очень длинное название теста можно считать диагностическим сигналом. Такие сигнал можно рассматривать с разных точек зрения, но в любом случае, как правило они свидетельствуют о наличии или возможном возникновении проблемы. Минимально возможные варианты: В тестируемом коде реализуется неправильная архитектура Это не модульный, а возможно интеграционный тест Неправильная реализация собственно тестового модуля Разработчик не выспался и(или) употреблял какие-либо трансформаторы сознания в избыточных количествах Раз уж вы привели пример, то давайте на него и посмотрим. Для начала я попробую извлечь из английского названия вашего теста русский смысл: "Удостовериться в том, что URL, на который перенаправляется клиент после обработк последней транзакции, должен содержать URL соответствующий профилю возврата на страницу, если тип транзакции реализует профиль возврата." Давайте предположим как может выглядеть собственно тело такого теста: [Test] public void redirect_to_profile_url_test() { Transaction trn = new TransactionCreateRecurringProfile(); this.executionContext.Add(trn); this.executionContext.ExecuteTransaction(); Assert.AreEqual(this.recurringProfileUrl, this.executionContext.RedirectUrl); } В первом приближении все вроде кратко, симпатично и читабельно. Давайте предположим как может выглядеть тест, который удостоверяется в обратном: [Test] public void do_not_redirect_to_profile_url_test() { Transaction trn = new TransactionDoesNotCreateRecurringProfile(); this.executionContext.Add(trn); this.executionContext.ExecuteTransaction(); Assert.AreNotEqual(this.recurringProfileUrl, this.executionContext.RedirectUrl); } Естественно мы предполагаем что вся подготовительная перед тестом работа вынесена из собственно тестов: [TestFixture] public class RedirectAfterTransactionTest { private ExecutionContext executionContext; private String recurringProfileUrl; [SetUp] public void setUp() { this.executionContext = new RealExecutionContext(); this.recurringProfileUrl = "http://localhost/bla-bla-bla"; } } Обновление Как я именую тесты? Примерно так (из рабочего проекта): public class InMemoryRepositoryTest { @Test public void insert_test() {} @Test public void find_test() {} @Test public void delete_test() {} @Test public void update_data_with_same_id_test() {} @Test public void cant_update_if_item_with_id_not_exists_test() {} @Test public void do_not_insert_other_type_data_test() {} @Test public void insert_with_merge_test() {} @Test public void update_with_merge_test() {} } Имя тестового класса ссылается на наименование тестируемого класса в коде. А имен тестовых методов обозначают имена тестируемых методов в тестовом классе и пытаются кратко сообщить об условиях тестирования каждого конкретного метода. Я предпочитаю сильно не увлекаться с подробностями в наименованиях тестовых методов потому что по мере рефакторинга кода, наименование метода может рассинхронизироваться с содержанием. Это особенно справедливо для случаев, когда проект верстается под сильным временным прессингом со стороны заказчика. Правила именования - это в некотором роде Дао или Кунг-Фу... Необходимо соблюдат баланс. Если имя тестового метода длиннее, чем сам код, содержащийся в тестовом методе то разработчику, который рефакторит метод, может быть проще и быстрее прочитать и понять код, чем продираться сначала через литературные перипетии названия на английском языке.

Ответ 5



Думаю, тестовые методы тут не стоит выделять отдельно - в целом они подчиняются те же правилам именования, что и обычные методы. В числе этих правил - не давать метод слишком длинное и неудобочитаемое название. Краткость - сестра таланта, как писал один русский писатель. Для всеобъемлющего описания того, что именно тестирует метод, человечество изобрело комментарии, в которых можно растекаться мыслью по древу (хотя злоупотреблять не стоит и там). Ваш же вариант именования сильно похож на применявшуюся несколько столетий наза витиеватую манеру давать названия книгам, когда в названии описывается едва ли не весь сюжет книги. Например, полное название Робинзона Крузо звучит устрашающе-длинно: "Жизнь, необыкновенные и удивительные приключения Робинзона Крузо, моряка из Йорка прожившего 28 лет в полном одиночестве на необитаемом острове у берегов Америки бли устьев реки Ориноко, куда он был выброшен кораблекрушением, во время которого весь экипаж корабля кроме него погиб, с изложением его неожиданного освобождения пиратами; написанные им самим"

Ответ 6



Я считаю что название модульного теста должно быть ёмким. Особенно если тест пишетс в BDD стиле. Особенно если ваш фреймворк для тестирования умеет это раскладывать в читабельные отчёты. Нет никакой разницы, насколько длинное имя метода с точки зрения читающего тест программиста Есть только один важный аспект, где длина метода будет пропорциональна пониманию, что он тестирует - это отчёт, который будет читать другой человек, не программист. На мой взгляд ёмкие названия (пример): testShouldAlwaysPreparePathsAndConvertThemInToArray testShouldRegisterNamespacesForMultipleDirectories testShouldThrowExceptionIfDbIsMissingOrInvalid testShouldSetOptionsWhenConfigPassedToTheConstructor В любом приличном фреймворке для тестирования такие имена методов трансформируются в что-то подобное (пример): Test should always prepare paths and convert them in to array [OK] Test should register namespaces for multiple directories [OK] Test should throw exception if db is missing or invalid [OK] Test should set option whet config passed to the constructor [FAIL] Согласитесь, такое гораздо приятнее читать. Это можно показать кому угодно, ПМ наприме или фронтенд разработчику : ) и ему совсем не обязательно знать фреймворк, который вы используете или язык. Не останавливайтесь на длине метода. Делайте его на столько длинным, насколько этог требует сценарий. Да, конечно, в пределах разумного. Несколько не с руки будет читать такой тест, если он из-за ширины экрана, в лучшем случае разобъётся на несколько строк, а то и вовсе обрежется.

Ответ 7



Меня учили что юнит тест всегда имеет шаблон названия: когдаТогда whenThen @Test whenStringArgInThenArgAddInArrayList() { Item item = new Item(); item.getList.add("a"); assertThat(item.getList[0].get, is("a")); }

воскресенье, 9 июня 2019 г.

Unit-тестирование работы с файловой системой

Возможно ли unit-тестирование класса, основная задача которого состоит во взаимодействии с файловой системой? Ну например, мне нужно протестировать класс, который предоставляет возможность записывать/читать биты из файла. Каким образом я могу это сделать? Можно, конечно, записать файл с какими-то тестовыми данными, а в самом тесте проверять что считываются именно те данные. Мне это кажется каким-то костылем. Так ли это и какие есть еще способы протестировать работу такого класса?


Ответ

Давайте попробуем пройтись по задачке в стиле TDD:
Какие публичные методы должен иметь данный класс? Пусть это будет только чтение Read(path, offset, count) и запись Write(name, offset, count) Пока мы не пишем их код, пока нам надо придумать, как бы мы их могли протестировать? Напишем несколько тестов (которые, кстати, помогут нам более точно описать поведение и интерфейс класса):
Прочитать что-то из файла и проверить, что прочиталось именно то что надо. Что должно произойти, если отдана команда прочитать 100 байт из файла размером 50 байт? Вернуть 50 байт, вернуть ошибку/исключение, или вернуть 100 байт из которых последние 50 - пустые? Выбирайте вариант и пишите на него тест. Что должно произойти, если файл не существует или недоступен? Опять же, выбирайте "правильное" поведение и пишите на него тест. Разрешаем ли мы передавать отрицательное значение offset, и что оно будет означать (например чтение с конца, а не с начала). Пишем тест. Будем ли мы поддерживать чтение/запись по локальной сети? Длинные имена файлов (260+)? Запись в системные папки (с требованием админских прав)? Что еще должно работать и что может пойти не так .. продумайте желаемое поведение и напишите на него тесты.
Повторите то же для метода записи в файл. Не стремитесь сразу предусмотреть ВСЁ. Учитывайте только то что вам понадобится в обозримом будущем (согласно вашему ТЗ). Теперь можно сделать паузу, и обобщить все те моменты и тонкости которые всплыли при проектировании тестов. Окинуть их взглядом еще раз, прикинуть какие ситуации требуют приватных методов и функций, общих для чтения и записи (например, проверка существования файла, или размера). А вот только теперь можно приступать к написанию кода самого класса чтения/записи в файл! Встретили какое-то еще условие, или граничный случай в процессе разработки - отлично, напишите на него тест, а потом код. В процессе эксплуатации класса выявился баг - тоже отлично, пишите на него тест, и только потом исправляйте баг (и тестом проверяйте, что он исправлен, и что в процессе исправления ничего из другого не сломалось).

понедельник, 27 мая 2019 г.

Java. TDD. Как проверить в assertThat() что объект имеет определенный тип (как instanceof)

Надо в тесте проверить что определенный объект имеет определенный тип.
boolean res = (desc[2][7] instanceof Place); assertThat(res,is(true));
Что-бы вот так не писать... Есть ли какой-то метод в util?


Ответ

Нужно использовать org.hamcrest.CoreMatchers.instanceOf
Ответ: https://stackoverflow.com/a/12404813/4828657

воскресенье, 12 мая 2019 г.

Как покрыть тестами конструктор класса в java?

Есть код, который по алгоритму Эвклида находит наибольший общий делитель.
import java.util.Scanner;
/** * Created by user on 24.11.2015. * По данным двум числам 1 public static int Euclid(int a, int b) { //конструктор if (a == 0 || b == 0) { //Рассматривается случай, когда одно из if (a == 0) { //делимых равно нулю return b; } else { return a; } } else { if (a > b) { return Euclid(a % b, b); //рекурсивно вызовется } // алгоритм, если будет остаток от деления большего if (b > a) { // числа на меньшее и наоборот return Euclid(a, b % a); } else return Euclid(a % b, b);
} } public static void main(String[] args) {
Scanner sc1 = new Scanner(System.in); //ввод с клавиатуры Scanner sc2 = new Scanner(System.in); int a = sc1.nextInt(); int b = sc2.nextInt(); System.out.println(Euclid(a,b)); } }
Несмотря на то, что программа работает, я решил по практиковаться на ней в разработке через тестирование.
Пишу тест:
import org.junit.Test;
import static org.junit.Assert.*; public class EuclidTest {
@Test public void testEuclid() throws Exception {
int result = new Euclid(234, 45); //в этой строке ошибка компиляции assertEquals(9, result, 1e-9); } }
В строке, где я объявляю result мне показывает ошибку компиляции.
Когда я переписал класс, заменив конструктор методом с другим названием всё прошло как по маслу. Связи с этим вопросы:
Можно ли покрыть конструктор тестами так, чтобы не вызвать ошибку компиляции? Как это сделать?


Ответ

public static int Euclid(int a, int b) { //конструктор
Дело в том, что это у вас не конструктор, а статический метод, возвращающий int. Конструктор был бы такой (он возвращает объект класса Euclid):
public Euclid(int a, int b) { //конструктор
Вообще, конструктор здесь не нужен никоим образом. Вам не нужно хранить какое-то состояние, так что и объекты создавать незачем.
Переименуйте ваш метод euclid с маленькой буквы, как положено по стандарту именования и пишите вот такой тест:
import org.junit.Test;
import static org.junit.Assert.*; public class EuclidTest {
@Test public void testEuclid() throws Exception { assertEquals(9, euclid(234, 45), 1e-9); } }

вторник, 12 марта 2019 г.

Выражение Assert при тестировании метода, который обновляет объект через сторонний сервис

Код
public class Company { public string Name { get; set; } }
public class Customer { public string Surname { get; set; } public string Name { get; set; } public Company Company { get; set; } }
public class CustomerDto { public int Id { get; set; }
[Required] public string Surname { get; set; }
[Required] public string Name { get; set; }
public string SecondName { get; set; } }
public class CustomerService { private readonly IGenericRepository _customerRepository;
public CustomerService(IGenericRepository customerRepository) { _customerRepository = customerRepository; }
public CustomerDto Add(CustomerDto dto) { //Validation if (dto == null) { throw new ArgumentNullException(nameof(dto)); } var validContext = new System.ComponentModel.DataAnnotations.ValidationContext(dto); Validator.ValidateObject(dto, validContext);
//Mapping by AutoMapper Mapper.Initialize(cfg => cfg.CreateMap()); var entity = Mapper.Map(dto);
_customerRepository.Insert(entity); _customerRepository.Save();
dto.Id = entity.Id;
return dto; } }

Юнит-тестирование CustomerService.Add()
Необходимо создать юнит-тест для метода Add(). Изначально, вообще была попытка реализовать этот метод через TDD, т.е. сначала создать для него юнит-тест и только затем имлемантацию. Но я не смог этого сделать так как не разобрался за что конкретно отвечает этот метод и надо ли вообще писать для него тест
Но два юнит-теста я сделал и вот как все это выглядит:
[TestClass] public class Customer_Test { [TestMethod] public void Add_Test() { //Arrange //Use NSubstitude for mocking var mockRepository = Substitute.For>();
//Act var service = new CustomerService(mockRepository); var dto = new CustomerDto { Surname = "Shakhabov", Name = "Adam" }; var addedDto = service.Add(dto);
//Assert Assert.IsNotNull(addedDto); }
[TestMethod] [ExpectedException(typeof(ValidationException))] public void Add_TestException() { //Arrange var mockRepository = Substitute.For>();
//Act var service = new CustomerService(mockRepository); var dto = new CustomerDto { Surname = "Shakhabov", }; var addedDto = service.Add(dto);
//Assert //expectedException ValidationException } }
Вот что (цель) должен сделать метод: Добавить в базу данных объект на основе полученного DTO-объекта и затем обратно вернуть этот DTO-объект, но уже с обновленным полем Id, значение для которого сформировала СУБД.
Вот как (задачи) делает это метод:
Получив DTO-объект, сразу верифицирует его; Используя библиотеку AutoMapper, проецирует DTO на сущность, которая в свою очередь создается с нуля; Используя объект-репозиторий, добавляет сущность в контекст. Сохраняет контекст в результате чего источник данных (СУБД) инициализирует поле Id у сущности (entity); Инициализирует поле dto.Id полем entity.Id и возвращает DTO.

Вопрос
Правильно ли были сформулированы требования для метода Add() и соответственно построены юнит-тесты на их основе? Какие тесты (выражения Assert) создаются для методов с подобной логикой, где целью является инициализировать свойство DTO-объекта значением, получаемое сторонним сервисом (в данном случаи объект-репозиторий)?


Ответ

Смотрим в метод Add. Что делает метод добавления? Добавляет новую сущность в БД и присваивает ей уникальный идентификатор.
С точки зрения возвращаемого значения - нужно вернуть только идентификатор, так как весь объект остался без изменений. Когда DTO приходит в метод добавления - идентификатор нулевой, то есть не заполнен. Когда сущьность (entity) будет добавлена в БД, ей будет присвоен новый уникальный идентифкатор. Так как используется тип данных int, то иднетификатор обязательно будет положительным (больше нуля) и уникален.
Конвертацию из DTO в объект сущности БД перенести внутрь DTO или добавить конструктор в сущность БД, который примет в качестве параметра DTO-шку. Про конвертацию подробнее: либо в объекте Customer сделать конструктор, который будет принимать объект типа CustomerDto, где внутри произвести присвоение полей с помощью AutoMapper, либо в CustomerDto сделать метод ToCustomer(), внутри которого создастся объект Customer и будут присвоены поля, который вернет объект типа Customer. Думаю, что с точки зрения логики, метод Add не должен делать эту конвертацию, так как его задача взять объект, добавить его и на основании результата уже будем знать, прошло ли все успешно.
Пример кода метода ToCustomer() в классе CustomerDto, для этого возможно понадобится подключить using для типа Customer и для AutoMapper, но это не беда, вот что я имел в виду:
public Customer ToCustomer() { //Mapping by AutoMapper Mapper.Initialize(cfg => cfg.CreateMap()); var entity = Mapper.Map(dto); return entity; }
По-хорошему и валидацию сделать бы более компактной и красивой, но это уже на ваше усмотрение. Еще один совет, инкапсулируйте в репозитрорий методы Insert и Save внутрь одного метода, который выполнит это (назовите его Create или Add). Ну и навесьте try...catch внутри метода, к примеру, если что-то пошло не так и упало исключение - тогда метод вернул -1, а в тестах вы уже будете знать, что если меньше нуля - то тест не прошел.
Касаемо тестов, у вас два сценария:
метод отработал успешно и без исключения - нужно проверить, что идентификатор не нулевой (больше нуля); пробуем подсунуть некорректные данные, которые вызовут исключение.
Соответственно, тестовый метод, проверяющий успешную работу должен проверить, что идентификатор не нулевой. Пример проверки: C# unit test, how to test greater than Там где ожидается исключение - не должно быть Assert проверок.
Касаемо TDD - это сложная практика, которая нарабатывается с опытом. Проще начать с написания простых классов через TDD, а потом уже переходить к более сложным сценариям. Чтобы писать через TDD, нужно иметь четкое представление того, как должна работать та или иная функциональность, которую вы хотите реализовать с ее помощью.
А еще, сейчас я занимаюсь приведением большого проекта в порядок с помощью одного вспомогательного компонента под названием StyleCop, так вот он не рекомендует использовать знак нижнего подчеркивания в именовании свойств, полей, параметров, переменных и прочего. Ну это так, к слову. Надеюсь, что мои советы и пояснения достаточно ясны и не вызовут противоречий.

понедельник, 24 декабря 2018 г.

Как протестировать junit вывод в консоль?

> public void add(Item item) {
> if (!item.header.equals(null)) {
> this.items[this.index] = item;
> this.index++;
> } else {
> System.out.println("Please header enter.");
> } }
хочу добавить тест на
} else {
System.out.println("Please header enter.");
используя конструкцию
assertThat(???, is(???));
Но что сюда подставлять если у меня консольное приложение? А метод void?


Ответ

Не завязывайте приложение на логирование в консоль. Используйте отдельный объект-логгер, которому будуте передавать строку и уровень логирования. Это стандарт, так делается во множестве крупных проектов.
Приложения работают на серверах, там некому пялиться в монитор и читать логи, поэтому сообщения пишутся туда, куда удобно разработчикам и админам — в файл, в сокет, отправляются по http, что угодно ещё. Могут и в stdout, как в вашем случае, но должен быть выбор. И если в stdout, то System.out.println находится в коде логгера, а не в том месте, где происходит логирование.
И когда вам нужно будет протестировать логгер, вы просто подсунете вместо него тестовый объект, в котором будете проверять, что такой-то метод с такой-то строкой был вызван.
Уже есть множество написанных логгеров, я когда-то использовал log4j, но не могу сравнить с другими.

четверг, 4 октября 2018 г.

Как осваивать тестирование?

В разработке я относительно недавно и несколько недель назад задумался над тем, что не тестирую свой код. Непорядок. Начал читать книгу Кент Бека про TDD. Уловил главную идею - "красный" - "зеленый" - "рефакторинг". Ну и что желательно двигаться маленькими шажками, чтобы проще было. Начал кодить в таком стиле и понял, что порой просто захожу в тупик, не зная, что тестировать и как. Например, отправка почты, как тестировать? Отправлять себе письмо? По сути, я тестировал полностью весь класс, стараясь покрыть каждый метод хоть небольшим тестом, чтобы удостовериться, что он работает. Я задумался, возможно стоит не полностью покрывать код тестами, а только те части, где сложная логика.
Опытные разработчики, посоветуйте, как лучше осваивать тестирование, стоит ли дальше двигаться в направлении TDD или может нужно повременить, опыта поднабраться в тестировании. Что нужно тестировать и как? Возможно есть правила, которые вы выработали для себя.


Ответ

Вы все делаете правильно - нужно писать тесты с самого начала и стремится к большему % покрытию кода. То, что Вы не можете придумать, как и что протестировать с опытом пройдет, а в данный момент можно тестировать, как получается. Относительно того как тестировать методы/классы или реализуемую ими функциональность то могу предложить сначала определится с методологией тестирования для начала посмотрите на: тестирования по стратегии белого ящика и тестирования по стратегии чёрного ящика
Что нужно тестировать и как?
По моему мнению и опыту тестировать в первую очередь нужно всю «нетипичную» логику. То есть те специфичные вещи которые Вы пишите для данного приложения, а не то что данные из базы выгружаются верно (хотя это тоже нужно протестировать) или метод действительно открывает файл.
Я стараюсь в первую очередь тестировать самые важные куски кода которые в случае если будут изменены обязательно должны сигнализировать разработчику (не прохождением теста), что логика работы стала отлична от ожидаемой (тестом), что должно обязать разработчика актуализировать тест.
Относительно того как тестировать – для критически важных участков кода я применяю сразу несколько тестов: на функциональность (черный ящик) – подаю все возможные значения на вход и проверяю на соответствие выходного значения или выбрасывания исключения, или наличие/отсутствие побочного эффекта, и код с помощью критериев покрытия MC/DC

Как осваивать тестирование?

В разработке я относительно недавно и несколько недель назад задумался над тем, что не тестирую свой код. Непорядок. Начал читать книгу Кент Бека про TDD. Уловил главную идею - "красный" - "зеленый" - "рефакторинг". Ну и что желательно двигаться маленькими шажками, чтобы проще было. Начал кодить в таком стиле и понял, что порой просто захожу в тупик, не зная, что тестировать и как. Например, отправка почты, как тестировать? Отправлять себе письмо? По сути, я тестировал полностью весь класс, стараясь покрыть каждый метод хоть небольшим тестом, чтобы удостовериться, что он работает. Я задумался, возможно стоит не полностью покрывать код тестами, а только те части, где сложная логика.
Опытные разработчики, посоветуйте, как лучше осваивать тестирование, стоит ли дальше двигаться в направлении TDD или может нужно повременить, опыта поднабраться в тестировании. Что нужно тестировать и как? Возможно есть правила, которые вы выработали для себя.


Ответ

Вы все делаете правильно - нужно писать тесты с самого начала и стремится к большему % покрытию кода. То, что Вы не можете придумать, как и что протестировать с опытом пройдет, а в данный момент можно тестировать, как получается. Относительно того как тестировать методы/классы или реализуемую ими функциональность то могу предложить сначала определится с методологией тестирования для начала посмотрите на: тестирования по стратегии белого ящика и тестирования по стратегии чёрного ящика
Что нужно тестировать и как?
По моему мнению и опыту тестировать в первую очередь нужно всю «нетипичную» логику. То есть те специфичные вещи которые Вы пишите для данного приложения, а не то что данные из базы выгружаются верно (хотя это тоже нужно протестировать) или метод действительно открывает файл.
Я стараюсь в первую очередь тестировать самые важные куски кода которые в случае если будут изменены обязательно должны сигнализировать разработчику (не прохождением теста), что логика работы стала отлична от ожидаемой (тестом), что должно обязать разработчика актуализировать тест.
Относительно того как тестировать – для критически важных участков кода я применяю сразу несколько тестов: на функциональность (черный ящик) – подаю все возможные значения на вход и проверяю на соответствие выходного значения или выбрасывания исключения, или наличие/отсутствие побочного эффекта, и код с помощью критериев покрытия MC/DC

воскресенье, 30 сентября 2018 г.

Как правильно выбрать название для юнит-теста?

Это был обычный будний день, я начал писать очередной обычный тест, но при написании названия теста что-то пошло не так:
testCustomerRedirectUrlAfterSelectLastTransactionProcessShouldContainsUrlToConfirmRecurringProfilePageWhenTransactionTypeIsCreateRecurringProfile
Примерно на середине я начал осознавать что происходит что-то не то, но я не остановился и дописал название теста до конца. Коллеги не оценили такое длиннющее название теста, но мне оно нравится. Сейчас используется что-то вроде testCorrectChangeCustomerRedirectUrl, а детали того что тест тестирует скрыты внутри теста.
Я не могу понять это лвлап, либо я устал под вечер.
Вопрос: Каковы критерии выбора хорошего названия теста?
UPD: Пожалуйста, приведите названия тестов из ваших проектов.


Ответ

Название для тестов не надо выбирать красивое. Название нужно выбирать функциональное и по возможности короткое -- чтобы по минимальному количеству символов было понятно, что и под какими условиями он тестирует, а также чтобы можно было отличить конкретный тест от тысяч других.
Распространенные шаблоны наименования такие:
TestClass_TestMethod_ConditionAndExpectedResult (для юнит-тестов) TestMethod_Condition_ExpectedResult (для юнит-тестов) ConditionAndExpectedResult (для интеграционных тестов, которые тестируют целые блоки функциональности через некоторую входную точку)
При этом если у вас в частях Condition/ExpectedResult слишком много разных условий/результатов, объединенных по "И", то вам нужно пересмотреть либо главную цель теста (что он тестирует), либо главное условие, которое отличает тест от других, и разбить его на несколько отдельных тестов.

Я ратую прежде всего за то, что название должно быть информативным, но при этом настолько коротким, насколько возможно. Тут точно так же как и со всем остальным кодом -- чем меньше кода, тем меньше времени нужно чтобы его понять, тем проще разбираться с ним. Судя по вашему названию, это не простой маленький юнит-тест, а что-то побольше, поэтому совсем коротким названием тут не обойтись. В вашем случае, если тест класс у вам уже для Customer, я бы ограничился названием SelectLastTransactionProcess_CreateRecurringProfile_RedirectUrlShouldContainConfirmation. В вашем названии много повторений слов: transaction, url, recurring profile и т.д. Они избыточны.
Между длиной и информативностью названия безусловно должен быть баланс. Достаточно, чтоб названия были понятны разработчикам внутри проекта, которые уже обладают контекстом и способны понять, о чем этот тест. И если для этого нужно длину названия сделать не 30 символов, а 40, сделайте. Но не нужно стремиться к тому, чтоб названия были понятны каждому встречному, попутно рассказывая обо всей системе, поскольку это приводит к таким вот километровым названиям, в которых сложно разобраться. Посмотрите на свой обычный код, посмотрите на названия методов. Вы ведь не описываете в названии метода каждую его деталь, каждую строчку? Вы описываете нечто главное, а остальное спрятано внутри. Точно так же и с тестовыми методами.

Кто-нибудь может ещё рассказать что делать с названиями интеграционных тестов, > где 100500 различных условий? К примеру биллинг, принимает на вход 5-10 видов прайс-листов и в зависимости от их комбинаций и комбинаций их параметров (тоже примерно 5-10) должен выдавать различные суммы на выход. Контейнер отправляемый в биллинг занимает ~30 строчек. Если в стиле поста называть тесты то это будет testPayoutDebtorToGateOntopGateTransactionAmountGateToCreditorFlatPayinDebtorTo‌​GateOntopTotalTransactionAmountGateToCreditorOntopTransactionAmount, причем не с полным контекстом. Нужно рассчитать итоговую сумму исходя из того что прайс-листы могли быть настроены следующим образом: Тип комиссии прайс-листа типа основной сделки - inside/total, комиссия 0.1% + 0.5руб cверху, расчет вести от оборота. Второй комиссионный прайс-лист ontop/gate.. ещё в таком же стиле раз 10 и пара неявных зависимостей прайс-листов друг от друга.
Я бы сказал, что вам нужен параметрический тест. Это такой тест, который одним кодом тестирует разные наборы данных. Например, в терминах тестового фреймворка xUnit параметрический тест метода Calculator.Add() может выглядеть так:
[Theory] // это параметрический тест [InlineDate(1, 2, 3)] // наборы данных, т.е. параметры теста [InlineDate(0, 1, 1)] [InlineDate(-1, 1, 0)] public void Calculator_Add_ShouldReturnCorrectResult(int a, int b, int expectedResult) { var result = Calculator.Add(a, b); Assert.AreEqual(result, expectedResult); }
С параметрическими тестами вам не нужно перечислять все условия в названиях -- название содержит только суть теста, что именно тестируется, конкретный кейс. В вашем случае это что-то вроде РасчетИтоговойСтоимости_ПоОсновнойИДополнительнойСделке (на английский сами переведете, вам область понятнее). А выглядеть ваш тестовый метод может так (при этом в xUnit тестовые данные можно подставлять из свойства и даже из отдельного объекта):
[Theory] [PropertyData("PriceData")] // тестовые данные находятся в отдельном свойстве public void РасчетИтоговойСтоимости_ПоОсновнойИДополнительнойСделке( ComissionType mainComissionType, Comission mainComission, CalculationType mainCalculationType, ComissionType auxComissionType, Comission auxComission, CalculationType auxCalculationType, double expectedPrice) { // код теста }
public static IEnumerable PriceData { get { // данные можете захардкодить, а можете прочитать из файла return new[] { new object[] { // основная сделка ComissionType.InsideTotal, new Comission(0.1, 0.5), CalculationType.ОтОборота, // дополнительная сделка ComissionType.OnTopGate, new Comission(0.42, 0.99), CalculationType.ОтБалды, // результат 100500 }, // и так далее для других пар }; } }
Я думаю во многих тестовых фреймворка есть возможность создания параметрических тестов. Способ задания параметров может отличаться, но суть останется та же: данные выносятся в секцию с данным, в названии теста остается только название кейса.
Дополнительную информацию о параметрических тестах можной найти в блоге Сергея Теплякова.