📜 ⬆️ ⬇️

A simple but illustrative example of using TDD

I, like many programmers, heard quite a lot and read about TDD practices. I know about the benefits of good code coverage with unit tests - and about the harm of not having it - in commercial projects, but I couldn’t apply TDD for various reasons. Having started the other day to write my game project, I decided that this is a good opportunity to try. As it turned out, the difference in comparison with the usual approach can be felt even with the implementation of the simplest class. I will write this example in steps and at the end I will describe the results that I saw for myself. I think the topic will be useful to those who are interested in TDD. From more experienced colleagues I would like to hear comments and criticism.

I will not describe the theory, you can easily find it yourself. The example is written in Java, TestNG is used as the Unit-test framework.

Task


I started with the development of a base class for a combat unit - a unit. At a basic level, I need the unit to have a health margin and damage it can do to other units.

It would seem that it could be simpler:
')
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; } } 


The implementation is very naive. Surely, when I start using this class, I’ll have to add more convenient methods, a constructor, etc. But so far I do not know what is needed and what is not, and I don’t want to write too much immediately.

So, this is what happened with the traditional head-on method. Now we will try to implement the same class through TDD.

Apply TDD


In reality, I did not write the above implementation, initially no class Unit exists. We start by creating a test class.

 @Test public class UnitTest { } 


We start thinking about the requirements for a class of a unit. The first thing that comes to mind is that it would be nice to be able to create a unit, setting its health and damage. So we write.

 @Test public class UnitTest { @Test public void youCreateAUnitGivenItsHealthAndDamage() { new Unit(100, 25); } } 


The test, of course, does not even compile - we do so that it passes.

 public class Unit { public Unit(int health, int damage) { } } 


There is nothing to refactor. We write the following test - I want to be able to find out the current health of the unit.

 @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()); } } 


The test fails due to a compilation error - the Unit class does not have a getHealth method. Rule the code to pass the test.

 public class Unit { private int health; public Unit(int health, int damage) { this.health = health; } public int getHealth() { return health; } } 


There is nothing to refactor again. We think further - probably it would be nice if the unit knew how to take 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); } } 


Rule the code to pass the test.

 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) { } } 


Oh yeah, the damage taken should be deducted from the health of the unit. I will write a separate test for this.

 @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()); } } 


The first test that falls due to class behavior. Rule

 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; } } 


Here you can already a little refactor. Here you can leave and so, but I'm used to the fact that the getters are at the end of the class.

 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; } } 


Moving on. Our unit already has a health margin and can do damage. Teach him to damage other units!

 @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); } } 


We finish a class of a unit.

 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; } } 


Understandably, if our unit has dealt damage to another unit, its health should decrease.

 @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()); } } 


Freshly written test falls - we fix the class of the unit.

 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; } } 


Bring some shine: the damage variable may be final, a parameter in the takeDamage method would be nice to rename so as not to be confused with a class variable.

 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; } } 


Next, you need to write tests that health cannot fall below zero, if it is at zero a unit must be able to say that it is dead, etc. In order not to add extra volume, I will stop here. I think it’s enough to understand the example and we can draw some conclusions.

findings


  1. The time spent on the implementation of the simplest class is several times longer than with the implementation of "head-on" - this is what so often scares managers and not owning this technique TDD programmers.
  2. You can compare the first naive implementation and the latest, obtained through TDD. The main difference is that the latest implementation is really object-oriented, with a unit you can work as with an independent object, ask its state and ask to perform certain actions. The code that will work with this class will also be more object oriented.
  3. In addition to the class itself, we received a complete set of tests for it. I know from my own experience that it is hard to imagine a greater good for the developer than a code completely covered in tests. From the same experience, if tests are written after the code, it can be difficult to provide full coverage - you will surely forget to write a test for something, something will look too simple to test, etc. The tests themselves are often difficult and cumbersome, because One test is tempted to test several aspects of the code under test. Here we got a set of simple, easy-to-understand tests that will be much easier to maintain.
  4. We received live code documentation! Any person can read the names of the methods in the test in order to understand the idea of ​​the author, the purpose and behavior of the class. To understand the code, having this information, it will be much easier and it is not necessary to distract colleagues with requests to explain what this is all about.
  5. The previous points are already well known, but I made one new conclusion for myself - when developing through TDD, it is much better thought out what I want to get from the class, its behavior and use cases. Having a good understanding of already developed components, it will be clearer how to write more complex ones.
  6. Not relevant to this example, but I wanted to add one more item here. When developing through tests, you adequately evaluate the complexity of the tasks - you always know what is already working, what remains to be completed. Without tests, there is often a feeling that everything is already written, but it turns out that a considerable amount of time is still needed for debugging and revision.


I understand that the example is very simple and I ask you not to find fault with this. The topic is not about how to write a class of two fields, but about the fact that you can see the benefits of TDD even on such an elementary example.

Thank you all for your attention. Learn progressive programming techniques and enjoy your work!

Source: https://habr.com/ru/post/151639/


All Articles