4

TDD proponents claim that it results in better design and decoupled objects. I can understand that writing tests first enforces the use of things like dependency injection, resulting in loosely coupled objects. However, TDD is based on unit tests - which test individual methods and not the integration between objects. And yet, TDD expects design to evolve from the tests themselves.

So how can TDD possibly result in a better design at the integration (i.e. inter-object) level when the granularity it addresses is finer than that (individual methods)?

1

7 Answers 7

7

I disagree with your assumption that unit tests have to be method-scope. Unit tests don't have to be limited to testing individual methods, they can test interactions between objects too.

The difference between a unit test and an integration test is that the integration test requires some kind of external resource (database, queue, file system, etc.) to be present. The unit test would use mocks to stand in for things at the boundaries of the application.

There is an idea that unit tests should be small. The wikipedia article on Unit testing says:

In computer programming, unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures are tested to determine if they are fit for use.1 Intuitively, one can view a unit as the smallest testable part of an application. In procedural programming, a unit could be an entire module, but is more commonly an individual function or procedure. In object-oriented programming, a unit is often an entire interface, such as a class, but could be an individual method. [2] Unit tests are created by programmers or occasionally by white box testers during the development process.

So the idea of what constitutes a unit seems very flexible to me.

There's no reason a unit test can't instantiate different objects and test the results of using them together, that can be a valuable way of testing that the code is internally self-consistent.

5
  • That's not what I'm asking (actually I asked that in another question). My point is: does TDD really result in better design beyond the unit level? How is that possible, given that TDD advocates unit tests (although, as you say, it is entirely possible to do integration tests as well)?
    – Gigi
    Commented Oct 2, 2013 at 18:05
  • @Gigi: I was addressing your assumption that TDD can only test at the method level, I don't think that's necessarily true. Unit tests can test internal consistency of the application code, they only stop short of verifying interaction with external systems. That's what I was trying to get across by distinguishing between unit- and integration-tests. If unit tests were necessarily method-scope then I would agree their helpfulness would be extremely limited. Commented Oct 2, 2013 at 18:10
  • 1
    I understand, and your point is perfectly valid. However, the purist approach is that TDD is unit tests only. It is with the purist approach that I am taking issue. Nonetheless, your input is useful and I'd like to invite you to post an answer to my other question on this exact topic: stackoverflow.com/questions/19142855/… .
    – Gigi
    Commented Oct 2, 2013 at 18:16
  • @Gigi "the purist approach is that TDD is unit tests only" - which purist stated this? As Nathan Hughes says TDD can be used equally well at an interaction or a unit level, where the integration tests will drive the creation of unit tests to satisfy the integration test.
    – Fresh
    Commented Oct 2, 2013 at 21:30
  • 1
    How about Uncle Bob? butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd The three rules are specific about unit tests.
    – Gigi
    Commented Oct 3, 2013 at 15:21
3

It depends. In TDD it doesn't have to be that there is a one-to-one relationship between a test and a class. Refactoring can produce many classes that are essentially under one set of tests. In that sense, those class interactions are tested.

That being said, there are boundaries where you would mock out certain interactions, whether using a mocking framework, or just in terms of decoupling. Those boundaries are subject to different testing, that is beyond TDD. In terms of TDD, you make the surface and the complexity of creating the coupling as small as reasonably possible and then the actual coupling doesn't get tested by TDD.

The coupling should generally be a line or two of code. If it is too complicated, you may need to test the coupling mechanism independent of the whole system, but that gets back to one of those boundaries.

Then you are left with an issue which is beyond TDD - I wrote a class or group of classes to do A, with the idea that class or group of classes B would interact with it. But did I mess that up? Is there a subtle difference in what I was thinking when I wrote one vs what I was thinking when I wrote the other? And of course in a multi-person project each side might not have been written by the same person.

That is integration testing, acceptance testing, QA, etc. It is test after, not test driven, even if your acceptance tests are written before, it isn't really the same.

4
  • 1
    Re your first paragraph, if you are testing class interactions, then they aren't unit tests in the strictest sense (they're integration tests). Re the rest, if I understand you correctly, class relationships are beyond the scope of TDD. Then how is the claim that TDD results in better design justified?
    – Gigi
    Commented Oct 2, 2013 at 18:12
  • On point one, TDD isn't about achieving unit tests in the strictest sense. It is simply not one of its goals. On the rest, the TDD drives these class relationships, as does the overall architecture, by having things being in testable units one of the important drivers of the design.
    – Yishai
    Commented Oct 2, 2013 at 21:30
  • As I commented on other answers, TDD seems to usually be defined in terms of red-green-factor and the three rules, which speak only of unit tests. I can accept that some people have a broader definition, but if these criteria are the 'real' TDD, then I'm interested in knowing if it can really be applied to a larger design as so many people proclaim so religiously.
    – Gigi
    Commented Oct 3, 2013 at 15:28
  • @Gigi, you are bringing your own definition of unit tests and reading it into what they write. They are simply not limiting unit tests to test which test one class without involving another. And the main advocates cannot really be described as purists. Anyway, the purist definition is contrived. Can you not use ArrayList in your class because then you are testing an integration between it and your class? Same thing if you write the class instead of taking one from the JDK.
    – Yishai
    Commented Oct 3, 2013 at 22:29
0

I think you want to look at integration tests, where you might mock out the classes which are the individual components and their expected results and perform actions on a collection of those.

You should set the output of test or mocked objects to known/expected values, you are then purely testing methods which interact on those specific objects.

Take an example where you have two classes, Person and Car for example. The individual class based unit tests would test say functions on each of those classes Person.SetName() and Car.SetMake() for example.

Your integration test might be the testing of a CarParkService where you'd pass in a Car and Person in a known state (potentially mocked) and call the method ParkACar(Person p, Car c). With this way of working you're purely testing the integration of the person and the car within the context of a service.

You can test this a lot further, but as far as I view it, in a simple sense integration tests will test the combination of classes/methods as a group (such as a service).

3
  • Like the other answers, what you say is valid. However I am questioning whether TDD can possibly result in a better overall design when it is concerned with the unit level and not integration per se.
    – Gigi
    Commented Oct 2, 2013 at 18:47
  • I suppose it would depend on what you mean by TDD. TDD is not limited to unit tests in my book, I see it as a wider discipline regarding writing the expectations of your functionality before you develop it. Whilst in its simplest/purest form it may be unit tests for single classes, I believe it does/should extend to integration tests between groups of classes and functionality. Testing the way individual classes fit together helps route out design issues by noticing when dependencies are difficult to provide or when the inputs and outputs of methods are not easy to provide or validate. Commented Oct 3, 2013 at 7:01
  • When researching TDD, the most common two things I see are the red-green-refactor cycle and the three rules (butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd). They seem pretty focused on unit testing and nothing else, although I understand people may adopt different practices as they deem practical.
    – Gigi
    Commented Oct 3, 2013 at 15:25
0

Unfortunately the definition of unit test is very fuzzy. One of the most cited definitions is done through the properties the tests should have, which are

  • runs fast
  • is isolated (no interactions between tests, i.e. run them in any order and always get the same result)
  • requires no external configuration
  • provides a consistent pass/fail result It's from Roy Osherove's book, the art of unit testing.

If I were to add a definition, I'd say a unit test asserts against the result of a logical path through a method where you have control over the result of all dependencies involved (fwiw, that's just my definition).

Imagine you have to create a method that checks if a string is valid, and if so does something, if not does something different. This is how we could write that method if we want it to be unit testable:

class SomeClass
{
    IStringAnalyzer stringAnalizer;
    ILogger logger;

    public SomeClass(IStringAnalyzer stringAnalyzer, ILogger logger)
    {    
        this.logger = logger;
        this.stringAnalyzer = stringAnalyzer;
    }


public void SomeMethod(string someParameter)
    {

    if (stringAnalyzer.IsValid(someParameter))
    {
        //do something with someParameter
    }else
    {
        logger.Log("Invalid string");
    }
}

stringAnalyser and logger are dependencies because we want to be able to control what they return (through a stub) and how they are used (through a mock).

This is so you can write a test like this (that has all the nice characteristics that Roy describes in his book):

[Test]
public void SomeMethod_InvalidParameter_CallsLogger
{
    Rhino.Mocks.MockRepository mockRepository = new Rhino.Mocks.MockRepository();
    IStringAnalyzer s = mockRepository.Stub<IStringRepository>();
    s.Stub(s => s.IsValid("something, doesnt matter").IgnoreParameters().Return(false);
    ILogger l = mockRepository.DynamicMock<ILogger>();
    SomeClass someClass = new SomeClass(s, l);
    mockRepository.ReplayAll();

    someClass.SomeMethod("What you put here doesnt really matter because the stub will always return false");

    l.AssertWasCalled(l => l.Log("Invalid string"));
}

The benefits are that the "several concerns" are all decoupled and they are very easy to spot when you are doing the test first. You'd see that you would need to leave the logic of StringAnalizer out of the class you are testing because the need for controlling if a string is valid or not would be evident.

That test makes use of an isolation framework (rhino mocks) and I explain what that is here: http://blinkingcaret.wordpress.com/2012/11/20/interaction-testing-fakes-mocks-and-stubs/

Regarding the discussion of TDD really produces better designs all I can say is this: http://blinkingcaret.wordpress.com/2012/10/02/tdd-bdd-add-every-other-method-that-promises-software-quality-here-and-the-tea-tasting-lady/ And this: http://vimeo.com/9270320

0

So how can TDD possibly result in a better design at the integration (i.e. inter-object) level when the granularity it addresses is finer than that (individual methods) ?

Because granularity evolves as a result of TDD :)

The fact that the entry point in a test (Act part in Arrange Act Assert) is a single method doesn't mean all resulting production code will be contained in that method at the end of the TDD cycle.

A (the most ?) important step in TDD is refactoring. Through refactoring, code can be moved to another method or another class.

0

To answer your question, I would say, what you state is right, partly.

TDD makes extensive use of unit tests, but also acceptance test. If you look at the full TDD test cycle described in Growing Object-Oriented Software, Guided by Tests, it is:

  1. Write a failing acceptance test
  2. Write a failing unit test
  3. Make the failing test pass
  4. Refactor
  5. Repeat steps 2-4 until the failing acceptance test passes

By definition acceptance tests are end-to-end tests that start from the user input level through all your application stack (including domain objects, external services and databases).

Because of this you will not only test your components and define interaction there, but also base them on overarching acceptance tests that proof your components work together.

This will provide you with enough certainty so you are able to impose structural and architectural improvements on your code whenever required.

If you want to see this in action check this TDD session with Uncle Bob.

7
  • Are the acceptance tests automated like unit tests?
    – Gigi
    Commented Oct 3, 2013 at 15:30
  • sure, you can use any test framework, but also specialized frameworks like Fit, Fitnesse or Specflow (the latter is more specialied towards BDD)
    – Jesko R.
    Commented Oct 3, 2013 at 17:12
  • Right, so you're saying that for TDD we should have automated tests through the UI (that way it's from user input level)?
    – Gigi
    Commented Oct 3, 2013 at 17:18
  • 1
    Ideally, yes. However,in practice, you will have to use a UI pattern like MVVM and MVC/MVP to make testing feasible. Full UI testing will be much more a topic for system and regression testing. But if you abstract your UI logic from presentation correctly it's not very risky that way.
    – Jesko R.
    Commented Oct 3, 2013 at 17:26
  • Good point about MVVM - I'll give it some thought and see if I can understand how it scales.
    – Gigi
    Commented Oct 3, 2013 at 17:48
0

I know this post is quite old, but I thought it would be helpful to give a practical example of how UNIT testing can indeed assist SYSTEM design.

I have pasted below some code that I think answers this question from the OP:

TDD is based on unit tests.. and not the integration between objects...So how can it possibly result in a better design at the integration (i.e. inter-object) level.

One answer to this is to define and test the API of the entire system, by example, in a collection of mocks for all the units, based on interfaces (not implementations) BEFORE doing any implementation at all. Then write ALL the unit tests referring to this collection, and by definition the tests should all pass.

Continue tweaking the overall system design until happy with it, and then implement the interfaces one by one, in each case switching out the interface mock for the new class implementation and refactoring until it passes.

In the following example there are just two minimal interfaces called IUnit1 and IUnit2 and a class which is responsible for defining the API between them, called MockApi. There is then a test class with unit tests for the two units, each of which can run with either the mocked interface or the implementation when available. Finally there are implementations called Unit1 and Unit2.

This example uses Moq and XUnit, but obviously could apply to any platform.

using Moq;
using Xunit;

namespace TDD_with_API
{
    public interface IUnit1
    {
        string GetMessage(IUnit2 unit2);
    }
    public interface IUnit2
    {
        string GetExclamation();
        string Name { get; set; }
    }
    public class MockApi
    {
        public MockApi()
        {
            // SET UP THE MOCKS AND EXPOSE THEIR OBJECTS AS PROPERTIES:
            var mock_unit1 = new Mock<IUnit1>();
            Unit1 = mock_unit1.Object;
            var mock_unit2 = new Mock<IUnit2>();
            Unit2 = mock_unit2.Object;
            //    ..etc for all the units in the API

            // DEFINE THE API BY EXAMPLE:
            mock_unit2.Setup(u => u.GetExclamation()).Returns("Hello");
            mock_unit2.Setup(u => u.Name).Returns("World");
            mock_unit1.Setup(u => u.GetMessage(It.IsAny<IUnit2>()))
                .Returns((IUnit2 u) => u.GetExclamation() + " " + u.Name + "!");
        }
        public IUnit1 Unit1 { get; set; }
        public IUnit2 Unit2 { get; set; }
    }
    public class UnitTests
    {
        private MockApi _api;
        public UnitTests()
        {
            _api = new MockApi();
        }
        [Theory]
        [InlineData(true)] // Comment this line out until class implemented
        [InlineData(false)]
        public void Test_Unit1(bool testThisUnitForReal)
        {
            IUnit1 unitToTest = testThisUnitForReal ?
                new Unit1() : _api.Unit1;
            Assert.Equal(unitToTest.GetMessage(_api.Unit2), "Hello World!");
        }
        [Theory]
        [InlineData(true)] // Comment this line out until class implemented
        [InlineData(false)]
        public void Test_Unit2(bool testThisUnitForReal)
        {
            IUnit2 unitToTest = testThisUnitForReal ?
                new Unit2() : _api.Unit2;
            Assert.Equal(unitToTest.GetExclamation(), "Hello");
            Assert.Equal(unitToTest.Name, "World");
        }
    }
    public class Unit1 : IUnit1
    {
        public string GetMessage(IUnit2 unit2)
        {
            return string.Format("{0} {1}!",
                unit2.GetExclamation(),unit2.Name);
        }
    }
    public class Unit2 : IUnit2
    {
        public string Name { get; set; } = "World";
        public string GetExclamation()
        {
            return "Hello";
        }
    }
}

This site is temporarily in read-only mode and not accepting new answers.

Not the answer you're looking for? Browse other questions tagged .