Thursday, 21 February 2013

The Two Main Techniques in Test-Driven development, part 1

Today, I'd like to write a bit about different techniques for test-driving development. There are actually two of them which I'd like to discuss: Need-Driven Development and Triangulation. Part one is about NDD, Triangulation shall be covered in part two.

Need-Driven Development

History

Need-Driven Development is a term that's less commonly used nowadays. It was coined in a paper Mock Roles Not Objects, but has since been somewhat abandoned. The authors of the mentioned paper wrote a book later on and the term Need-Driven Development is missing from it. Anyway, the approach itself has not changed and is by far one of the most popular.

Description

Imagine you're building a plane and the requirement for it is to transport a guy named John from Poland to US. Imagine you start with computer simulation (in a fake context) of putting a guy in the air - he would fall, the simulation says. Ok, so we need to put him on something - we introduce floor. But the floor would fall as well. So we need something to hold it in the air - we introduce engine. But we need to keep the balance in the air somehow - we introduce wings. And so on, and so on. This is the philosophy of NDD - start with the main thing you need without having anything and then figure out what else you need to do this and the figure out what this "else" needs...

NDD is an outside-in approach to development using unit tests and mock objects that stand for the context which does not yet exist. This way, we start with what our current concrete object needs and shape its context (which is filled in by mock objects) based on those needs. So in this approach, mocks are used as a tool for discovering interactions - we're free to shape the mocked interface anyway we like so that they're best suited to interact with the concrete object we're dealing with now.

As the fathers of NDD put it:

We use Mock Objects to let us write the code under test as if it had everything it needs from its environment. This process shows us what an object’s environment should be so we can then provide it.

Example

Let's take as an example the case from my previous post, which specified a simplified logic for logging in:

  1. When supplied credentials are valid, the Log-in process shall setup response as positive and redirect me to the home page
  2. When supplied credentials are invalid, the log-in process shall setup the response as negative and redirect me to the login page.
[Test]
public void 
ShouldSetResponseAsPositiveAndRedirectToHomePageWhenPerformedWithValidCredentials()
{
  //GIVEN
  var response = Substitute.For<Response>();
  var validCredentials = Substitute.For<UserCredentials>();
  var homePage = Substitute.For<Page>();
  var loginPage = Substitute.For<Page>();
  var loggingIn = new LoggingIn(homePage, loginPage);
  validCredentials.AreValid().Returns(true);

  //WHEN
  loggingIn.PerformUsing(validCredentials);

  //THEN
  response.Received().SetUpAsSuccessful();
  homePage.Received().RedirectTo();
}

The specification shows us that from the perspective of LoggingIn class, it's most comfortable to describe its behavior in the context of collaborators like Response, Page and Credentials that provide certain services in the form of methods. Thus, we have discovered three new abstractions (along with some methods they need to provide), that don't need to have concrete implementations in order to run specifications for LoggingIn class - we fill these abstractions with mocks, so that specs compile and run. When we're finished describing the LoggingIn object, we can proceed with describing real implementations for all the discovered abstractions the same way we did for LoggingIn class (by the way, the specifications for LoggingIn are always based on mocks to provide behavior isolation - we never change mocks to real objects in unit specs).

Principles

These are the main principles of Need-Driven Development:

  1. Start the development from the outermost layer (i.e. the inputs) and dig deeper and deeper, layer by layer, reaching the boundaries of the application
  2. Derive how interfaces should look like from what their clients need them to provide
  3. Derive not only interfaces (method signatures), but also protocols (e.g. which behaviors of discovered abstractions are expected by the class in terms of which we discovered them and how they should be handled)
  4. Think of the application as a web of collaborating objects sending messages to each other
  5. Mock only the types you own - mocks are primarily design tools, not isolation tools (although the latter is still somewhat significant)
  6. Avoid getters, try to follow "Tell, don't ask" advice most of the time
  7. Each test creates a need for either:
    • new implementation in the tested object
    • new collaborator to appear
    • new way of communicating with existing collaborator to appear (e.g. calling different method on a collaborator or a collaborator returning different result from already existing method)
  8. Distinguish between Value Types and "objects". Value types are merely a more intelligent and domain-oriented data, while "objects" are service providers.

Related Concepts

Lean

The need-driven development is closely related to Lean principles for pulling value from demand rather than pushing it from implementation. Thus, following the NDD leads to writing only the necessary code and avoiding adding features that aren't needed. This is because the only code that gets written is the one directly needed by code that already exists. The need for the first production code to exist are the acceptance tests, and those come directly from the customer. This way, NDD fits well into a cohesive value stream, starting with the customer and ending on the code itself. Thus, NDD is well aligned with lean principles.

"Needs interfaces" and "capabilities interfaces"

Another related concept is that of "Needs vs capabilities in interfaces", coming from the book Essential Skills for the Agile Developer. One important lesson from it is to always design interfaces (e.g. method signatures) according to client needs and what the client would use. After the second client is introduced that needs a different interface to the same services, the way to go is to provide it with its own Needs Interface and extracting the common logic in so called Capabilities Interface that both of these Need Interfaces use. At that time, these Need Interfaces most often become mere adapters or facades to the more general interface. The lesson from the authors of Essential Skills for the Agile Developers is compatible with the NDD philosophy, guiding us in our design and refactoring attempts.

Programming by Intention

If you remember my post about Programming By Intention and how it compares to TDD, then you probably already noticed the similarities between Programming By Intention and NDD. As a matter of fact, when I mentioned TDD in that post, I had NDD in mind.

Applicability

The technique is most useful when we have well-defined acceptance criteria and usually comes into play after creating and executing a failing acceptance test that defines the next increment of functionality we need to provide. Also, it's easier to apply when we know the high-level work-flows the application needs to support (usually the case of enterprise systems). Then we discover lower-level work-flows that result from the higher-level work-flows and, layer by layer, we reach the boundaries of the system.

No comments: