Надо в тесте проверить что определенный объект имеет определенный тип.
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));
}
}
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);
}
}
Закрыт. На этот вопрос невозможно дать объективный ответ.
Ответы на него в данный момент не принимаются.
Хотите улучшить этот вопрос? Переформулируйте вопрос,
чтобы на него можно было дать ответ, основанный на фактах и цитатах, отредактировав его.
Закрыт 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 вынуждает разработчика писать чище
и проще, заранее придумывать апи, держать цикломатическую сложность в тонусе.
Код
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, так вот он не рекомендует использовать
знак нижнего подчеркивания в именовании свойств, полей, параметров, переменных и прочего.
Ну это так, к слову. Надеюсь, что мои советы и пояснения достаточно ясны и не вызовут
противоречий.
Возможно ли unit-тестирование класса, основная задача которого состоит во взаимодействии
с файловой системой? Ну например, мне нужно протестировать класс, который предоставляет
возможность записывать/читать биты из файла. Каким образом я могу это сделать? Можно,
конечно, записать файл с какими-то тестовыми данными, а в самом тесте проверять что
считываются именно те данные. Мне это кажется каким-то костылем. Так ли это и какие
есть еще способы протестировать работу такого класса?
Ответы
Ответ 1
Давайте попробуем пройтись по задачке в стиле TDD:
Какие публичные методы должен иметь данный класс? Пусть это будет только чтение Read(path,
offset, count) и запись Write(name, offset, count).
Пока мы не пишем их код, пока нам надо придумать, как бы мы их могли протестировать?
Напишем несколько тестов (которые, кстати, помогут нам более точно описать поведение
и интерфейс класса):
Прочитать что-то из файла и проверить, что прочиталось именно то что надо.
Что должно произойти, если отдана команда прочитать 100 байт из файла размером 50
байт? Вернуть 50 байт, вернуть ошибку/исключение, или вернуть 100 байт из которых последние
50 - пустые? Выбирайте вариант и пишите на него тест.
Что должно произойти, если файл не существует или недоступен? Опять же, выбирайте
"правильное" поведение и пишите на него тест.
Разрешаем ли мы передавать отрицательное значение offset, и что оно будет означать
(например чтение с конца, а не с начала). Пишем тест.
Будем ли мы поддерживать чтение/запись по локальной сети? Длинные имена файлов (260+)?
Запись в системные папки (с требованием админских прав)?
Что еще должно работать и что может пойти не так .. продумайте желаемое поведение
и напишите на него тесты.
Повторите то же для метода записи в файл. Не стремитесь сразу предусмотреть ВСЁ.
Учитывайте только то что вам понадобится в обозримом будущем (согласно вашему ТЗ).
Теперь можно сделать паузу, и обобщить все те моменты и тонкости которые всплыли
при проектировании тестов. Окинуть их взглядом еще раз, прикинуть какие ситуации требуют
приватных методов и функций, общих для чтения и записи (например, проверка существования
файла, или размера).
А вот только теперь можно приступать к написанию кода самого класса чтения/записи в файл!
Встретили какое-то еще условие, или граничный случай в процессе разработки - отлично,
напишите на него тест, а потом код.
В процессе эксплуатации класса выявился баг - тоже отлично, пишите на него тест,
и только потом исправляйте баг (и тестом проверяйте, что он исправлен, и что в процессе
исправления ничего из другого не сломалось).
Нашел такое задание в курсе по 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
не понимаю как можно написать сначала юнит-тест, а потом реализовать метод
Это нормально. Этого никто не понимает. Не зная предметную область, не обсудив ТЗ
с заказчиком и с коллегами, не имея большого опыта разработки в целом и опыта разработки
в данной предметной области в частности, невозможно сразу начать писать тесты.
Те, кто уверяют, что нужно сразу писать красный тест, слегка лукавят. Сколько я ни
общался с такими разработчиками, сколько ни видел примеров в интернете, там всегда
пишется тест на уже знакомую тему. То есть данный разработчик участвовал по крайней
мере в одном проекте (а скорее в нескольких на протяжении ряда лет) в данной предметной
области. И вот на основании своего предыдущего многолетнего опыта он и способен написать
сперва тест. Но на самом деле, сперва был написан код: в предыдущих проектах.
А как дело касается новой предметной области, опять начинается написание сперва пробных
кусков кода, их переписывание, выбрасывание... Писать тест на код, который гарантированно
будет выброшен, нет смысла. И лишь после нескольких итераций выкристаллизовывается
некий каркас, по которому можно начинать писать тесты.
Поэтому, не расстраивайтесь, когда у вас не будет сразу получаться писать сперва
тесты и лишь потом код.
> 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, но не могу
сравнить с другими.
Наверное тяжело будет тем кто не читал 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.
Это был обычный будний день, я начал писать очередной обычный тест, но при написании названия теста что-то пошло не так:
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 строчек. Если в стиле поста называть тесты то
это будет
testPayoutDebtorToGateOntopGateTransactionAmountGateToCreditorFlatPayinDebtorToGateOntopTotalTransactionAmountGateToCreditorOntopTransactionAmount,
причем не с полным контекстом.
Нужно рассчитать итоговую сумму исходя из
того что прайс-листы могли быть настроены следующим образом: Тип
комиссии прайс-листа типа основной сделки - 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
Возможно ли unit-тестирование класса, основная задача которого состоит во взаимодействии с файловой системой? Ну например, мне нужно протестировать класс, который предоставляет возможность записывать/читать биты из файла. Каким образом я могу это сделать? Можно, конечно, записать файл с какими-то тестовыми данными, а в самом тесте проверять что считываются именно те данные. Мне это кажется каким-то костылем. Так ли это и какие есть еще способы протестировать работу такого класса?
Ответ
Давайте попробуем пройтись по задачке в стиле TDD:
Какие публичные методы должен иметь данный класс? Пусть это будет только чтение Read(path, offset, count) и запись Write(name, offset, count)
Пока мы не пишем их код, пока нам надо придумать, как бы мы их могли протестировать? Напишем несколько тестов (которые, кстати, помогут нам более точно описать поведение и интерфейс класса):
Прочитать что-то из файла и проверить, что прочиталось именно то что надо.
Что должно произойти, если отдана команда прочитать 100 байт из файла размером 50 байт? Вернуть 50 байт, вернуть ошибку/исключение, или вернуть 100 байт из которых последние 50 - пустые? Выбирайте вариант и пишите на него тест.
Что должно произойти, если файл не существует или недоступен? Опять же, выбирайте "правильное" поведение и пишите на него тест.
Разрешаем ли мы передавать отрицательное значение offset, и что оно будет означать (например чтение с конца, а не с начала). Пишем тест.
Будем ли мы поддерживать чтение/запись по локальной сети? Длинные имена файлов (260+)? Запись в системные папки (с требованием админских прав)?
Что еще должно работать и что может пойти не так .. продумайте желаемое поведение и напишите на него тесты.
Повторите то же для метода записи в файл. Не стремитесь сразу предусмотреть ВСЁ. Учитывайте только то что вам понадобится в обозримом будущем (согласно вашему ТЗ).
Теперь можно сделать паузу, и обобщить все те моменты и тонкости которые всплыли при проектировании тестов. Окинуть их взглядом еще раз, прикинуть какие ситуации требуют приватных методов и функций, общих для чтения и записи (например, проверка существования файла, или размера).
А вот только теперь можно приступать к написанию кода самого класса чтения/записи в файл!
Встретили какое-то еще условие, или граничный случай в процессе разработки - отлично, напишите на него тест, а потом код.
В процессе эксплуатации класса выявился баг - тоже отлично, пишите на него тест, и только потом исправляйте баг (и тестом проверяйте, что он исправлен, и что в процессе исправления ничего из другого не сломалось).
Надо в тесте проверить что определенный объект имеет определенный тип. boolean res = (desc[2][7] instanceof Place);
assertThat(res,is(true));
Что-бы вот так не писать... Есть ли какой-то метод в util?
Ответ
Нужно использовать org.hamcrest.CoreMatchers.instanceOf Ответ:
https://stackoverflow.com/a/12404813/4828657
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);
}
}
Код 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, так вот он не рекомендует использовать знак нижнего подчеркивания в именовании свойств, полей, параметров, переменных и прочего. Ну это так, к слову. Надеюсь, что мои советы и пояснения достаточно ясны и не вызовут противоречий.
> 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, но не могу сравнить с другими.
В разработке я относительно недавно и несколько недель назад задумался над тем, что не тестирую свой код. Непорядок. Начал читать книгу Кент Бека про TDD. Уловил главную идею - "красный" - "зеленый" - "рефакторинг". Ну и что желательно двигаться маленькими шажками, чтобы проще было.
Начал кодить в таком стиле и понял, что порой просто захожу в тупик, не зная, что тестировать и как. Например, отправка почты, как тестировать? Отправлять себе письмо? По сути, я тестировал полностью весь класс, стараясь покрыть каждый метод хоть небольшим тестом, чтобы удостовериться, что он работает. Я задумался, возможно стоит не полностью покрывать код тестами, а только те части, где сложная логика. Опытные разработчики, посоветуйте, как лучше осваивать тестирование, стоит ли дальше двигаться в направлении TDD или может нужно повременить, опыта поднабраться в тестировании. Что нужно тестировать и как? Возможно есть правила, которые вы выработали для себя.
Ответ
Вы все делаете правильно - нужно писать тесты с самого начала и стремится к большему % покрытию кода. То, что Вы не можете придумать, как и что протестировать с опытом пройдет, а в данный момент можно тестировать, как получается. Относительно того как тестировать методы/классы или реализуемую ими функциональность то могу предложить сначала определится с методологией тестирования для начала посмотрите на: тестирования по стратегии белого ящика и тестирования по стратегии чёрного ящика
Что нужно тестировать и как?
По моему мнению и опыту тестировать в первую очередь нужно всю «нетипичную» логику. То есть те специфичные вещи которые Вы пишите для данного приложения, а не то что данные из базы выгружаются верно (хотя это тоже нужно протестировать) или метод действительно открывает файл. Я стараюсь в первую очередь тестировать самые важные куски кода которые в случае если будут изменены обязательно должны сигнализировать разработчику (не прохождением теста), что логика работы стала отлична от ожидаемой (тестом), что должно обязать разработчика актуализировать тест. Относительно того как тестировать – для критически важных участков кода я применяю сразу несколько тестов: на функциональность (черный ящик) – подаю все возможные значения на вход и проверяю на соответствие выходного значения или выбрасывания исключения, или наличие/отсутствие побочного эффекта, и код с помощью критериев покрытия MC/DC
В разработке я относительно недавно и несколько недель назад задумался над тем, что не тестирую свой код. Непорядок. Начал читать книгу Кент Бека про TDD. Уловил главную идею - "красный" - "зеленый" - "рефакторинг". Ну и что желательно двигаться маленькими шажками, чтобы проще было.
Начал кодить в таком стиле и понял, что порой просто захожу в тупик, не зная, что тестировать и как. Например, отправка почты, как тестировать? Отправлять себе письмо? По сути, я тестировал полностью весь класс, стараясь покрыть каждый метод хоть небольшим тестом, чтобы удостовериться, что он работает. Я задумался, возможно стоит не полностью покрывать код тестами, а только те части, где сложная логика. Опытные разработчики, посоветуйте, как лучше осваивать тестирование, стоит ли дальше двигаться в направлении TDD или может нужно повременить, опыта поднабраться в тестировании. Что нужно тестировать и как? Возможно есть правила, которые вы выработали для себя.
Ответ
Вы все делаете правильно - нужно писать тесты с самого начала и стремится к большему % покрытию кода. То, что Вы не можете придумать, как и что протестировать с опытом пройдет, а в данный момент можно тестировать, как получается. Относительно того как тестировать методы/классы или реализуемую ими функциональность то могу предложить сначала определится с методологией тестирования для начала посмотрите на: тестирования по стратегии белого ящика и тестирования по стратегии чёрного ящика
Что нужно тестировать и как?
По моему мнению и опыту тестировать в первую очередь нужно всю «нетипичную» логику. То есть те специфичные вещи которые Вы пишите для данного приложения, а не то что данные из базы выгружаются верно (хотя это тоже нужно протестировать) или метод действительно открывает файл. Я стараюсь в первую очередь тестировать самые важные куски кода которые в случае если будут изменены обязательно должны сигнализировать разработчику (не прохождением теста), что логика работы стала отлична от ожидаемой (тестом), что должно обязать разработчика актуализировать тест. Относительно того как тестировать – для критически важных участков кода я применяю сразу несколько тестов: на функциональность (черный ящик) – подаю все возможные значения на вход и проверяю на соответствие выходного значения или выбрасывания исключения, или наличие/отсутствие побочного эффекта, и код с помощью критериев покрытия MC/DC
Это был обычный будний день, я начал писать очередной обычный тест, но при написании названия теста что-то пошло не так: 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 строчек. Если в стиле поста называть тесты то
это будет
testPayoutDebtorToGateOntopGateTransactionAmountGateToCreditorFlatPayinDebtorToGateOntopTotalTransactionAmountGateToCreditorOntopTransactionAmount,
причем не с полным контекстом.
Нужно рассчитать итоговую сумму исходя из
того что прайс-листы могли быть настроены следующим образом: Тип
комиссии прайс-листа типа основной сделки - 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