Search code examples
tddsolid-principles

Single Responsibility Principle, Test Driven Development, and Functional Design


I am fairly new to Test Driven Development and I just started learning the SOLID principles so I was hoping someone could help me out. I'm having some conceptual trouble understanding the Single Responsibility Principle in the context of developing unit tests and methods in the TDD paradigm. For instance, say I want to develop a method that deletes an item from a database- before my code would have looked like the following...

I'd start with defining a test case:

"Delete_Item_ReturnsTrue": function() {
    //Setup
    var ItemToDelete = "NameOfSomeItem";
    //Action
    var BooleanResult = Delete( ItemToDelete );
    //Assert
    if ( BooleanResult === true ) {
        return true;
    } else {
        console.log("Test: Delete_Item_ReturnsTrue() - Failed.");
        return false;
    }
} 

I'd run the test to make sure it failed, then I'd develop the method...

function Delete( ItemToDelete ) {
    var Database = ConnectToDatabase();
    var Query = BuildQuery( ItemToDelete );
    var QueryResult = Database.Query( Query );
    if ( QueryResult.error !== true ) {
    //if there's no error then...
       return true;
    } else {
       return false;
    }
}

If I'm understanding the Single Responsibility Principle correctly, the method that I originally wrote had the responsibilities of deleting the item AND returning true if there wasn't an error. So if I follow the SOLID paradigm the original method should be refactored to look something like...

function Delete( WhatToDelete, WhereToDeleteItFrom ) {
     WhereToDeleteItFrom.delete( WhatToDelete );
}

Doing the design as such, changed my method from a boolean function to a void function so now I can't really test the new method in the same manner I was testing my old method.

I guess I could test for thrown exceptions but then doesn't that make it an integration test rather than a unit test because there's no actual exception thrown in the method?

Do I design and implement an extra function which checks the database for use in my unit test?

Do I just not test it because it's void? How exactly does that work in TDD?

Do I pass in a mock repository and then return it after it's state has changed? Doesn't that just essientially bring me back to square one?

Do I pass in a reference to a mock repository and then just test against the repository after the method completes? Wouldn't that be considered a side effect though?


Solution

  • So the single responsibility principle says: They should be exact one reason when I need to change a function.

    So let's look at your function:

    function Delete( ItemToDelete ) {
        var Database = ConnectToDatabase();
        var Query = BuildQuery( ItemToDelete );
        var QueryResult = Database.Query( Query );
        if ( QueryResult.error !== true ) {
        //if there's no error then...
           return true;
        } else {
           return false;
        }
    }
    
    • Database changes: ConnectToDatabese() needs a change, but not this function
    • Changes in the db structure: BuildQuery() needs to be changed (maybe)
    • ...

    So on the first look, your function looks good. The naming on some places is a little bit confusing. A function should be start with a small letter. For example "connectToDatabase()". It is a little bit surprising that connectToDatabase returns an Object.

    The name BuildQuery seems to be wrong, because BuildQuery( myItem ) returns a query wich is deleting something.

    But I would never have such a long complicated function which just one testcase.

    You need to write more test cases. For the first test case you could write the function like that:

    function Delete( ItemToDelete) {
      return true
    }
    

    The next testcase could be "call the buildDeleteQuery function with the item id". At that point you need to think about how you call your db. If you have a dbObject, which does that you could mock that Object.

    function Delete( ItemToDelete ) {
       buildDeleteQuery( ItemToDelete.id )
       return true
    }
    

    Now more and more test cases. Remember that there will be also test cases for buildDeleteQuery. But for the outer function you could mock buildDeleteQuery.

    Now to answer some of your questions:

    • When you first write the test and then write the code, you will have testable code.
    • That code looks different, because the tests should be not to complicated. When you want easy tests you will automaticly have more smaller functions.
    • Yes, you will write a test for every function you call.
    • Maybe you will mock objects. And if you are not able to test your db call without a wrapping function you will create a function, which allows you to test that functionality.
    • Remember your tests are the documentation of your code. So keep them simple as possible.

    But the most important thing: Keep practicing! When you start with TDD it takes some time. A good and fun resource is: Uncle Bobs Bowling Kata Just download the slides from the website and look how tdd is done step by step.