DEV Community

tomtomdurrant
tomtomdurrant

Posted on

How to implement test driven development in the real world?

I really want to use TDD in my professional work to ensure it’s robust and stable but I’m struggling a bit in following the process as soon as I reach a little bit of complexity.

I was wondering how you all go about it?

Top comments (12)

Collapse
 
sargalias profile image
Spyros Argalias

Firstly, I second the recommendation for Kent Beck's book made by @Evgeniy.

The next thing is that I don't find TDD suitable for "prototyping", or trying things out with code until you have a general idea of what you need to do.

In other words, if I ask you "how would you do X", and your response is "no idea", then maybe you can't use TDD, because you might not know how you could possibly design the code. First you might try out some code until you get the general idea. Then your response may change to "Oh I see. I would have a random generator here, use the result like so and return blah". You can work with this.

That's because when doing TDD you're essentially asking "how do I want to call this method or class"? Writing the test means you have answered this question in your head. That's equivalent to asking "how do I design this feature?", at least from the point of view of the rest of the codebase. So to TDD is equivalent to having figured out a design, at least for the immediate test you want to write.

So, you either:

  • Feel comfortable and use TDD right off the bat.
  • Figure out how to design it first. Maybe prototype. Then go back and do TDD.
  • Or just ignore TDD. Create the feature, and then test it afterwards. If necessary, refactor and clean it up. The potential downside of this method is that you code until it works with little emphasis on design, so the code quality may turn out slightly worse.

Of course, in all cases you can go back and make changes, so don't worry about getting it perfect.

Also the more you use TDD, the better you'll get, so keep it up!

Collapse
 
slavius profile image
Slavius • Edited

Unless you have an abstraction on top of an existing business area everywhere it is really hard to do TDD. In the latter case TDD is only suitable for implementing change requests or small feature updates to existing code.

It is easy to write a test with framework of abstractions:

interface IAnimal {
  private AnimalType Type {get;set;}
  public Color FurColor {get;set;}
  public byte Age {get;set;}
  public string Name {get;set;}
  public IEnumerable<Leg> Legs {get;set;}
  public IEnumerable<Func> Actions {get;set;}
  public AnimalType GetType();
}
public void TestIAnimal() {
  IAnimal dog = null;
  ShouldNotThrow(() => { 
    dog = AnimalFactory.Create(nameof(dogType))
    .SetName("Rook")
    .SetAge(6)
    .SetFurColor(Color.Brown);
  });
  dog.ShouldNotBeNull();
  dog.GetType().ToString().ShouldBe(nameof(dogType));
  dog.Legs.Count().ShouldBe(4);
  dog.Actions().Any(f => nameof(f) == "Bark").ShouldBeTrue();
}

instead of just a plain disaster:

public void TestDog() {
  Dog dog = null; // does not compile, class Dog does not exist
  dog = new Dog(name: "Rook", age: 6, furColor: Color.Brown); // does not compile, constructor does not exist
  ShouldNotThrow(() => { dog.Bark(); }); // does not compile, Method .Bark() does not exist
  dog.Legs.Count().ShouldBe(4); // does not compile, no such property/accessor for Legs
  etc...
}

Starting a project with TDD might IMHO kill the project in question unless you're doing something you've already done before many times and you have very clear idea what to do at each step.

Collapse
 
sargalias profile image
Spyros Argalias

I think it depends. However I'm not so sure about the particular example above.

Sure, maybe if you're really experienced with TDD, you could work with that. But it might be best to start with very little code per test. That way you focus on testing a unit, a public function of the class, not the entire class in one test.

// You can substitute for interfaces, dependency injection and anything else.

test('dog should bark', () => {
  dog = new Dog('name', 'something', 'something else');
  const result = dog.bark();
  expect(result).toBe('woof');
});

test('dog should have correct furcolor', () => {
  dog = new Dog('name', 'something', 'something else');
  expect(dog.furcolor).toBe('something');
});

So you only test a small thing at a time, and design a small thing at a time. Also you can go in a loop of writing a single test, making it pass, repeat. It's not necessary to write more.

One of the important parts is the design, please see my previous comment for more details on that.

As I also mentioned, TDD is optional. If your code design is sufficient, and you don't require the benefits of TDD, then that's okay too.

Thread Thread
 
slavius profile image
Slavius • Edited

In a test you should test isolated feature.

In my example I tested correct instantiation of an object only. That's it. I didn't even test if the function Bark() produces any result, let alone a correct result based on constructor parameters. Only if it exists in a list of available object functions for a dog type object. I honestly don't think that's too much for a test content.

Do you think it is?

In contrast any other approach cannot even compile your test project as it cannot resolve Dog class.
Then if you fix it, it won't compile unless you implement missing properties.
Then you implement the properties but you miss getter logic (e.g. return age if only birth date was provided).
And so on.

I need some explanation from you.
Your test examples instantiate the dog class twice to compare immutable properties. You did not even change the object state, why should you write another test to make sure the dog can bark while not testing his fur color? I don't think that makes sense. It would, only if barking changes the fur color or if the fur color changes does in fact change the output of the Bark() function. But I assume that is not the case, is it?

I find it unusable that my tests do not compile and do not fail properly until I implement the missing code. That's a big no-no for my CI/CD pipeline...

Thread Thread
 
sargalias profile image
Spyros Argalias • Edited

Okay fair enough. Yeah I guess the example you gave didn't really have functionality, it was more of an object which just holds properties.

You're right that we need to test an isolated feature. However what is an isolated feature? It's subjective. For an object with getters and setters, I would argue that an isolated feature is a single property you can set and get. But someone could argue that the object can't be used until everything has been set properly. In that case the test would require setting everything, as you say.

So in the end it depends. If we don't need to set everything at once, then perhaps it would be easier to have one test where we only check the age getter and setter, and another test for the name, and so on... But of course it's up to requirements and personal preference.

Not compiling is acceptable when we do TDD, it's just something we have to work with.

Edit: For the CI/CD pipeline, we don't have to commit until both a test and implementation are working.

I'm repeating myself here but TDD is optional. If it doesn't work for your workflow or preference that's fine. Sometimes I can't use it either. Sometimes I just build and refactor later.

Thread Thread
 
slavius profile image
Slavius

My issue with not compiling is that it breaks my CI/CD pipeline. I'd like to create tests in my test project and the whole solution just compiles but the tests fail. This way my Jenkins/Bamboo/whatever can produce meaningful message to the developers in form of a failed test report instead of a bunch of compiler errors hidden in tons of output.

Thanks for discussion I realized some thinks I was not aware of. ;)

Thread Thread
 
sargalias profile image
Spyros Argalias

*I added a quick edit above about the CI/CD issue.

Collapse
 
tomtomdurrant profile image
tomtomdurrant

I definitely think TDD is worth doing, from my experience anyway. I think I might try and do the ‘figure out the design first’ method and then go back and refactor. That seems like the easiest method to get the ball rolling.

Collapse
 
slavius profile image
Slavius • Edited

I found it really hard to write tests against Interfaces, Classes, Properties and Methods that do not yet exist. They basically make my solutions to not compile and throw a bunch of errors. If you ever find a way, please let me know...

I mean if you write a complex test against non-existent code then you've actually already:

  • architectured new application feature according to a user story
  • decided where the business logic will be
  • made a naming convention decisions
  • made accessibility decisions (private, public, protected)
  • made OOP decisions (Interface, Class or static class with static method)
  • made OOP decisions part.2 (constructor, destructor, implements IDisposable)
  • made OOP decisions part.3 (constructor parameters with Dependency Injection)
  • made input output types and model decisions

If you've made all this (which IMHO takes most of the time), why didn't you just also write the method body?

Collapse
 
tomtomdurrant profile image
tomtomdurrant

I suppose it’s a question of ‘Are you doing TDD in a pure fashion?’ Vs ‘Do you want to be pragmatic about how you implement a good practice?’

Collapse
 
evgeniir profile image
Evgeniy

Try "Test Driven Development: By Example" book by TDD's author(Kent Beck).

Collapse
 
tomtomdurrant profile image
tomtomdurrant

Thanks! Will give it a look