Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TDD: Do I have to define everything my code should NOT do?

Problem

I'm using Test-Driven Development and having trouble making my tests define my code well enough. A simple example of my problem is as follows.

I have MyObject from which I want to call either methodA() or methodB() belonging to OtherObject depending on what argument MyObject receives in its own callMethod(int).

Anticipated code (and desired functionality)

This is essentially what I want the code to do - but I want to do it test first:

public class MyObject {

    private final OtherObject otherObject;

    public MyObject(OtherObject otherObject) {
        this.otherObject = otherObject;
    }

    public void callMethod(int i) {
        switch (i) {
        case 0:
            otherObject.methodA();
            break;
        case 1:
            otherObject.methodB();
            break;
        }
    }
}

Writing it test first

To achieve this I start by writing a test - check that methodA() is called when calling callMethod(0). I use JUnit and Mockito.

public class MyObjectTest {

    private final OtherObject mockOtherObject = mock(OtherObject.class);
    private final MyObject myObject = new MyObject(mockOtherObject);

    @Test
    public void callsMethodA_WhenArgumentIs0() {
        myObject.callMethod(0);
        verify(mockOtherObject).methodA();
    }
}

I create the classes/methods needed to get rid of errors and make the test pass by implementing MyObject's method like this:

public void callMethod(int i) {
    otherObject.methodA();
}

Next a test for the other option - calling callMethod(1)

@Test
public void callsMethodB_WhenArgumentIs1() {
    myObject.callMethod(1);
    verify(mockOtherObject).methodB();
}

And I get a final solution of:

public void callMethod(int i) {
    otherObject.methodA();
    otherObject.methodB();
}

The issue

This works but is clearly not what I want. How do I progress to the code I want using tests? Here I have tested for the behaviour I would like. The only solution I can think of is to write some more tests for behaviour I would not like to see.

In this example it would be okay to write 2 more tests to check that the other method is not being called but surely doing it like that is more of an issue in the general case. When there are more options, more complexity in which methods and how many different methods are called depending on the circumstances.

Say there were 3 methods in my example - would I have to write 3 tests to check the right method is called - then 6 more if I'm checking the 2 other methods aren't called for each of the 3 cases? (Whether you try and stick with one assertion per test or not you still have to write them all.)

It looks like the number of tests will be factorial to how many options the code has.

Another option is to just write the if or switch statements but techically it wouldn't have been driven by the tests.

like image 375
user3006756 Avatar asked Feb 14 '23 15:02

user3006756


2 Answers

I think you need to take a slightly bigger-picture view of your code. Don't think about what methods it should call, but think about what the overall effect of those methods should be.

  • What should the outputs and the side-effects be of calling callMethod(0)?
  • What should the outputs and the side-effects be of calling callMethod(1)?

And don't answer in terms of calls to methodA or methodB, but in terms of what can be seen from outside. What (if anything) should be returned from callMethod? What additional behaviour can the caller of callMethod see?

If methodA does something special that the caller of callMethod can observe, then include it in your test. If it's important to observe that behaviour when callMethod(0) happens, then test for it. And if it's important NOT to observe that behaviour when callMethod(1) happens, then test for that too.

like image 81
Dawood ibn Kareem Avatar answered Feb 17 '23 09:02

Dawood ibn Kareem


In regard to your specific example, I'd say you are doing it exactly right. Your tests should specify the behavior of your class under test. If you need to specify that your class doesn't do something under some circumstance, so be it. In a different example, this would not bother you. For instance, checking both conditions in this method probably would not raise any objections:

public void save(){
  if(isDirty)
     persistence.write(this);
}

In the general case, you are right again. Adding complexity to a method makes TDD harder. The unexpected result is that this is one of the greatest benefits of TDD. If your tests are hide to write, then your code is also too complex. It will be hard to reason about and hard to maintain. If you listen to your tests, you'll consider changing your design in a way that simplifies the tests.

In your example, I might leave it alone (it's pretty simple as is). But, if the number of case grows, I'd consider a change like this:

public class MyObject {

    private final OtherObjectFactory factory;

    public MyObject(OtherObjectFactory factory) {
        this.factory = factory;
    }

    public void callMethod(int i) {
        factory.createOtherObject(i).doSomething();
    }
}

public abstract class OtherObject{
    public abstract void doSomething();
}

public class OtherObjectFactory {
    public OtherObject createOtherObject(int i){
        switch (i) {
        case 0:
            return new MethodAImpl();
        case 1:
            return new MethodBImpl();
        }
    }
}

Note that this change adds some overhead to the problem you are trying to solve; I would not bother with it for two cases. But as cases grow, this scales very nicely: you add a new test for OtherObjectFactory and a new implementation of OtherObject. You never change MyObject, or it's tests; it only has one simple test. It's also not the only way to make the tests simpler, it's just the first thing that occurred to me.

The overall point is that if your tests are complex, it doesn't mean testing isn't effective. Good tests and good design are two sides of the same coin. Tests need to bite off small chunks of a problem at a time to be effective, just like code needs to solve small chunks of a problem at a time to be maintainable and cohesive. Two hands wash each other.

like image 41
tallseth Avatar answered Feb 17 '23 10:02

tallseth