23

Since I'm a TDD newbie, I'm currently developing a tiny C# console application in order to practice (because practice makes perfect, right?). I started by making a simple sketchup of how the application could be organized (class-wise) and started developing all domain classes that I could identify, one by one (test first, of course).

In the end, the classes have to be integrated together in order to make the application runnable, i.e. placing necessary code in the Main method which calls the necessary logic. However, I don't see how I can do this last integration step in a "test first" manner.

I suppose I wouldn't be having these issues had I used a "top down" approach. The question is: how would I do that? Should I have started by testing the Main() method?

If anyone could give me some pointers, it will be much appreciated.

Chris
  • 885
  • 8
  • 19
  • Do you have any high level test cases, that if passed will confirm that the application is working and that the problem has been solved? Something to prove that the whole thing works (keeping the UI out, of course). (I am a TDD newbie too.) – Abhijeet Kashnia Dec 31 '10 at 13:22
  • I don't have any high level test cases, and maybe that's what's missing. Only question is: how would such a high level test look like? The application only produces text in the console. Should I assert what is being written there? – Chris Dec 31 '10 at 13:38
  • When you say "integration", this does not mean that you want to test that the wiring up of objects is correct, but that all the classes together work as expected. Now, most people recommend that you should have some test cases that tell you if you are done with the work. What you have in the main function is proabably a test, as Marcus pointed out, one positive test. Main methods, you see, are the last methods to be written in a test driven application. I suggest you look at acceptance tests. This book might be interesting to you: http://www.growing-object-oriented-software.com/ – Abhijeet Kashnia Dec 31 '10 at 14:16
  • Yes, to clarify, I wrote "integration" but meant "wiring up". I'm actually reading that book now and they seem to recommend that you should set up an acceptance test as you describe. I suppose that implies that I should test what is actually being printed to the console, e.g. error messages? – Chris Dec 31 '10 at 14:31

7 Answers7

34

"Top-down" is already used in computing to describe an analysis technique. I suggest using the term "outside-in" instead.

Outside-in is a term from BDD, in which we recognise that there are often multiple user interfaces to a system. Users can be other systems as well as people. A BDD approach is similar to a TDD approach; it might help you so I will describe it briefly.

In BDD we start with a scenario - usually a simple example of a user using the system. Conversations around the scenarios help us work out what the system should really do. We write a user interface, and we can automate the scenarios against that user interface if we want.

When we write the user interface, we keep it as thin as possible. The user interface will use another class - a controller, viewmodel, etc. - for which we can define an API.

At this stage, the API will be either an empty class or a (programme) interface. Now we can write examples of how the user interface might use this controller, and show how the controller delivers value.

The examples also show the scope of the controller's responsibility, and how it delegates its responsibilities to other classes like repositories, services, etc. We can express that delegation using mocks. We then write that class to make the examples (unit tests) work. We write just enough examples to make our system-level scenarios pass.

I find it's common to rework mocked examples as the interfaces of the mocks are only guessed at first, then emerge more fully as the class is written. That will help us to define the next layer of interfaces or APIs, for which we describe more examples, and so on until no more mocks are needed and the first scenario passes.

As we describe more scenarios, we create different behaviour in the classes and we can refactor to remove duplication where different scenarios and user interfaces require similar behaviour.

By doing this in an outside-in fashion we get as much information as to what the APIs should be as possible, and rework those APIs as quickly as we can. This fits with the principles of Real Options (never commit early unless you know why). We don't create anything we don't use, and the APIs themselves are designed for usability - rather than for ease of writing. The code tends to be written in a more natural style using domain language more than programming language, making it more readable. Since code is read about 10x more than it's written, this also helps make it maintainable.

For that reason, I'd use an outside-in approach, rather than the intelligent guesswork of bottom-up. My experience is that it results in simpler, more strongly decoupled, more readable and more maintainable code.

Lunivore
  • 17,277
  • 4
  • 47
  • 92
  • This helps a lot, thank you! It sounds like I should use an overlying acceptance test in order to test-drive my application. – Chris Jan 01 '11 at 16:25
  • 1
    I've found the word "test" tends to cause people to think about pinning things down, stopping things from being broken, making sure things work, etc. Instead, try thinking of describing some examples of how things work. You're helping other people understand the value of your code and how the behaviour delivers the value so they can change it safely. That's why I (and other BDDers) use the words "scenario" and "example" instead. Hope that makes sense. It only becomes a test once the code is written. – Lunivore Jan 02 '11 at 01:50
2

If you moved stuff out of main(), could you not test that function?

It makes sense to do that, since you might want to run with different cmd-args and you'd want to test those.

Macke
  • 24,812
  • 7
  • 82
  • 118
  • Yes I could do that, and that's probably the easiest thing to do at this point. However, in the beginning, shouldn't I have started with some higher-level test in order test-drive the application? It feels that way, but maybe I'm making too much out of it. – Chris Dec 31 '10 at 14:43
  • Well, speccing input/output to the console in files is one way, but I think that works better for regression tests, however YMMV. The Mercurial DVCS project does exactly this in their suite, and it works pretty well for them, but they have pretty good commands to inspect the results of any operation. – Macke Jan 01 '11 at 11:53
0

You can test your console application as well. I found it quite hard, look at this example:

[TestMethod]
public void ValidateConsoleOutput()
{
    using (StringWriter sw = new StringWriter())
    {
        Console.SetOut(sw);
        ConsoleUser cu = new ConsoleUser();
        cu.DoWork();
        string expected = string.Format("Ploeh{0}", Environment.NewLine);
        Assert.AreEqual<string>(expected, sw.ToString());
    }
}

You can look at the full post here.

goenning
  • 6,514
  • 1
  • 35
  • 42
0

"bottom up" can save you a lot of work cos you already have low-level classes (tested) and you can use them in higher level tests. in opposite case you'd have to write a lot of (probably complicated) mockups. also "top down" often doesn't work as it's supposed: you think out some nice high-level model, test and implement it, and then moving down you realize that some of your assumptions were wrong (it's quite typical situation even for experienced programmers). of course patterns help here but it's not silver bullet too.


to have complete test coverage, you'll need to test your Main in any case. I'm not sure it's worth the effort in all cases. Just move all logic from Main to separate function (as Marcus Lindblom proposed), test it and let Main to just pass command-line arguments to your function.


console output is a simplest possible testing ability, and if you use TDD it can be used just to output the final result w/o any diagnosis and debug messages. so make your top-level function to return solid result, test it and just output it in Main

Andriy Tylychko
  • 15,967
  • 6
  • 64
  • 112
0

I recommend "top down" (I call that high level testing). I wrote about that:

http://www.hardcoded.net/articles/high-level-testing.htm

So yes, you should test your console output directly. Sure, initially it's a pain to setup a test so that it's easy to test console output directly, but if you create appropriate helper code, your next "high level" tests will be much easier to write. By doing that, you empower yourself to unlimited re-factoring potential. With "bottom up", your initial class relationship is very rigid because changing the relationship means changing the tests (lots of work and dangerous).

Virgil Dupras
  • 2,634
  • 20
  • 22
0

There was an interesting piece in MSDN magazine of December, which describes "how the BDD cycle wraps the traditional Test-Driven Development (TDD) cycle with feature-level tests that drive unit-level implementation". The details may be too technology specific, but the ideas and process outline sound relevant to your question.

Mathias
  • 15,191
  • 9
  • 60
  • 92
0

The main should be very simple in the end. I don't know how it looks like in c#, but in c++ it should look something like this :

#include "something"

int main( int argc, char *argv[])
{
  return TaskClass::Run( argc, argv );
}

Pass already created objects to the constructors of classes, but in unit tests pass mock objects.

For more info about TDD, take a look at these screencasts. They explain how to do agile development, but it also talks how to do TDD, with examples in c#.

BЈовић
  • 62,405
  • 41
  • 173
  • 273