- PVSM.RU - https://www.pvsm.ru -
Я, как и многие программисты, довольно много слышал и читал о практиках TDD [1]. О пользе хорошего покрытия кода юнит-тестами — и о вреде его отсутствия — я знаю по собственному опыту в коммерческих проектах, но применять TDD в чистом виде не получалось по разным причинам. Начав на днях писать свой игровой проект, я решил, что это хорошая возможность попробовать. Как оказалось, разницу по сравнению с обычным подходом можно почувствовать даже при реализации простейшего класса. Я распишу этот пример по шагам и в конце опишу результаты, которые для себя увидел. Думаю топик будет полезен тем, кто интересуется TDD. От более опытных коллег хотелось бы услышать комментарии и критику.
Теорию описывать не буду, ее можно легко найти самостоятельно. Пример написан на Java, в качестве Unit-test фреймворка использован TestNG.
Я начал с разработки базового класса для боевой единицы — юнита. На базовом уровне мне нужно чтобы юнит имел запас здоровья и урон, который он может наносить другим юнитам.
Казалось бы, что может быть проще:
public class Unit {
private int health;
private int damage;
public int getHealth() {
return health;
}
public int setHealth(int health) {
this.health = health;
}
public int getDamage() {
return damage;
}
public int setDamage(int damage) {
this.damage = damage;
}
}
Реализация очень наивная. Наверняка, когда я начну использовать этот класс, придется в него добавить более удобные методы, конструктор и т.д. Но пока я не знаю, что понадобится, а что нет, и не хочу сразу писать лишнее.
Итак, это то, что получилось традиционным методом «в лоб». Теперь попробуем реализовать тот же класс через TDD.
В реальности я не писал приведенную выше реализацию, изначально никакого класса Unit не существует. Мы начинаем с создания класса теста.
@Test
public class UnitTest {
}
Начинаем думать о требованиях к классу юнита. Первое, что приходит в голову — неплохо бы уметь создавать юнит, задавая его здоровье и урон. Так и пишем.
@Test
public class UnitTest {
@Test
public void youCreateAUnitGivenItsHealthAndDamage() {
new Unit(100, 25);
}
}
Тест, понятное дело, даже не компилируется — делаем так чтобы он прошел.
public class Unit {
public Unit(int health, int damage) {
}
}
Рефакторить пока нечего. Пишем следующий тест — я хочу иметь возможность узнать текущее здоровье юнита.
@Test
public class UnitTest {
@Test
public void youCreateAUnitGivenItsHealthAndDamage() {
new Unit(100, 25);
}
@Test
public void youCheckUnitHealthWithGetter() {
Unit unit = new Unit(100, 25);
assertEquals(100, unit.getHealth());
}
}
Тест падает из-за ошибки компиляции — метода getHealth у класса Unit нет. Правим код, чтобы тест прошел.
public class Unit {
private int health;
public Unit(int health, int damage) {
this.health = health;
}
public int getHealth() {
return health;
}
}
Рефакторить опять нечего. Думаем дальше — наверное было бы неплохо, чтобы юнит умел получать урон.
@Test
public class UnitTest {
@Test
public void youCreateAUnitGivenItsHealthAndDamage() {
new Unit(100, 25);
}
@Test
public void youCheckUnitHealthWithGetter() {
Unit unit = new Unit(100, 25);
assertEquals(100, unit.getHealth());
}
@Test
public void unitCanTakeDamage() {
Unit unit = new Unit(100, 25);
unit.takeDamage(25);
}
}
Правим код, чтобы тест прошел.
public class Unit {
private int health;
public Unit(int health, int damage) {
this.health = health;
}
public int getHealth() {
return health;
}
public void takeDamage(int damage) {
}
}
Ах да, полученный урон должен вычитаться из здоровья юнита. Напишу отдельный тест для этого.
@Test
public class UnitTest {
@Test
public void youCreateAUnitGivenItsHealthAndDamage() {
new Unit(100, 25);
}
@Test
public void youCheckUnitHealthWithGetter() {
Unit unit = new Unit(100, 25);
assertEquals(100, unit.getHealth());
}
@Test
public void unitCanTakeDamage() {
Unit unit = new Unit(100, 25);
unit.takeDamage(25);
}
@Test
public void damageTakenReducesUnitHealth() {
Unit unit = new Unit(100, 25);
unit.takeDamage(25);
assertEquals(75, unit.getHealth());
}
}
Первый тест, который падает из-за поведения класса. Правим.
public class Unit {
private int health;
public Unit(int health, int damage) {
this.health = health;
}
public int getHealth() {
return health;
}
public void takeDamage(int damage) {
health -= damage;
}
}
Тут уже можно немного порефакторить. Тут можно оставить и так, но я привык что геттеры находятся в конце класса.
public class Unit {
private int health;
public Unit(int health, int damage) {
this.health = health;
}
public void takeDamage(int damage) {
health -= damage;
}
public int getHealth() {
return health;
}
}
Двигаемся дальше. Наш юнит уже имеет запас здоровья и ему можно наносить урон. Научим его наносить урон другим юнитам!
@Test
public class UnitTest {
@Test
public void youCreateAUnitGivenItsHealthAndDamage() {
new Unit(100, 25);
}
@Test
public void youCheckUnitHealthWithGetter() {
Unit unit = new Unit(100, 25);
assertEquals(100, unit.getHealth());
}
@Test
public void unitCanTakeDamage() {
Unit unit = new Unit(100, 25);
unit.takeDamage(25);
}
@Test
public void damageTakenReducesUnitHealth() {
Unit unit = new Unit(100, 25);
unit.takeDamage(25);
assertEquals(75, unit.getHealth());
}
@Test
public void unitCanDealDamageToAnotherUnit() {
Unit damageDealer = new Unit(100, 25);
Unit damageTaker = new Unit(100, 25);
damageDealer.dealDamage(damageTaker);
}
}
Дорабатываем класс юнита.
public class Unit {
private int health;
public Unit(int health, int damage) {
this.health = health;
}
public void takeDamage(int damage) {
health -= damage;
}
public void dealDamage(Unit damageTaker) {
}
public int getHealth() {
return health;
}
}
Понятное дело, если наш юнит нанес урон другому юниту, у того должно уменьшиться здоровье.
@Test
public class UnitTest {
@Test
public void youCreateAUnitGivenItsHealthAndDamage() {
new Unit(100, 25);
}
@Test
public void youCheckUnitHealthWithGetter() {
Unit unit = new Unit(100, 25);
assertEquals(100, unit.getHealth());
}
@Test
public void unitCanTakeDamage() {
Unit unit = new Unit(100, 25);
unit.takeDamage(25);
}
@Test
public void damageTakenReducesUnitHealth() {
Unit unit = new Unit(100, 25);
unit.takeDamage(25);
assertEquals(75, unit.getHealth());
}
@Test
public void unitCanDealDamageToAnotherUnit() {
Unit damageDealer = new Unit(100, 25);
Unit damageTaker = new Unit(100, 25);
damageDealer.dealDamage(damageTaker);
}
@Test
public void unitThatDamageDealtToTakesDamageDealerUnitDamage() {
Unit damageDealer = new Unit(100, 25);
Unit damageTaker = new Unit(100, 25);
damageDealer.dealDamage(damageTaker);
assertEquals(75, damageTaker.getHealth());
}
}
Свеженаписаный тест падает — поправим класс юнита.
public class Unit {
private int health;
private int damage;
public Unit(int health, int damage) {
this.health = health;
this.damage = damage;
}
public void takeDamage(int damage) {
health -= damage;
}
public void dealDamage(Unit damageTaker) {
damageTaker.takeDamage(damage);
}
public int getHealth() {
return health;
}
}
Наведем немного блеска: переменная damage может быть final, параметр в методе takeDamage неплохо бы переименовать чтобы не путать с переменной класса.
public class Unit {
private int health;
private final int damage;
public Unit(int health, int damage) {
this.health = health;
this.damage = damage;
}
public void takeDamage(int incomingDamage) {
health -= incomingDamage;
}
public void dealDamage(Unit damageTaker) {
damageTaker.takeDamage(damage);
}
public int getHealth() {
return health;
}
}
Дальше нужно писать тесты на то, что здоровье не может упасть ниже нуля, если оно на нуле юнит должен уметь сказать что он мертв и т.д. Чтобы не добавлять лишнего объема, я остановлюсь тут. Думаю для понимания примера достаточно и можно сделать некоторые выводы.
Я понимаю что пример очень простой и прошу к этому не придираться. Топик не о том как написать класс из двух полей, а о том что можно увидеть преимущества TDD даже на таком элементарном примере.
Всем спасибо за внимание. Изучайте прогрессивные техники программирования и получайте удовольствие от работы!
Автор: nhekfqn
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/15288
Ссылки в тексте:
[1] TDD: http://en.wikipedia.org/wiki/Test-driven_development
Нажмите здесь для печати.