Friday, August 15, 2014

TDD: An Argument with Assumptions

TDD: An Argument with Assumptions

"Why do you have to write your tests first?  If you didn't, how would you know that those tests cover everything?  Why do we have a suite of tests?  What is the purpose of our suite of tests? So that we can refactor.  The suite of tests is not there to satisfy a QA group.  It's not there to prove to other people that our code works. It is there so that we can refactor." - Bob Martin

So that it is abundantly clear, according to Robert Martin, the purpose of testing is the ability to refactor.  Let's go over this argument.

Nutshell version:

If refactorability is paramount, then tests should be written first.
Refactorability is paramount.
Therefore, tests should be written first.

Of course, this argument alone is fairly weak, as it depends on a relationship between refactorability and when tests should be written.  So here is a longer version.

A. Refactorability is paramount.
B. Tests increase refactorability.
C. Writing tests before code is written ensures that all code written is covered by the test suite.
D. Ensuring that code is covered by the test suite increases refactorability.
E. Writing tests before code is written increases refactorability. (by C and D)
F. A greater number of tests increases refactorability more than fewer tests do.
G. A test suite which covers code is a greater number of tests than a test suite which does not cover code.
H. Writing tests before code is written increases refactorability more than writing tests afterwards does. (by F and G)
I. Methodologies of programming which increase refactorability should be chosen over those that either do not, or do so less. (implied by A)
J. Therefore, not only should tests be written, but they should be written before the code they cover.

Refactorability is Paramount

First of all, all code is able to be refactored.  Refactoring is changing code without breaking it, where "breaking it" means there there is some change in the mapping between inputs and outputs (including side effects).  Let's call this mapping between inputs and outputs "the behavior".

Refactoring is changing the code without changing the behavior.

So what is refactorability?  Refactorability is the ability to change code without breaking it.  It is the ability to change the code and have it still do what it's supposed to.

Refactorability is the ability to change code while it still does what it's supposed to do.

Refactorability is not absolute.  Refactorability is relative to the programmer.  What is refactorable to you is not refactorable to me, and as a result, refactorability is not a property of the code itself, which is counter-intuitive.  Code cannot be made to be refactorable.  Your ability as a programmer does that.

Do not be confused by the ease of saying "this code is refactorable", as this really only means "I have the ability to change this code and not break it".

Where does this ability come from?  How does one increase one's ability to change code but not break it?  There are three ways to gain such ability.

1. Use the program (hey, at least you now have some semblance of what this code is supposed to do)
2. Gain skill in programming. (generally OR specifically to the current code)
3. Write an automated test suite. (a programmatic way of describing intended behavior)

There are no other ways of gaining knowledge about side effects and boundary conditions, which is another way of saying there are no other ways of increasing/decreasing refactorability.

So is refactorability paramount? I don't know.  That's something for you to decide, but hopefully I've given you some tools to consider it wisely.  I think we can say that refactorability is beneficial.  It is something that ought to be considered and at least to some degree strived for.  But do note that if you say that refactorability is paramount, you're saying that the ability to change code without breaking it is more important than making code do what it's supposed to do.

Tests Increase Refactorability

"Given enough eyeballs, all bugs are shallow."
    - Linus's Law

Testing is a way to reduce the number of eyeballs required.  Tests provide this ability objectively, after all, tests only ever pass or they fail.

Tests are programmatic ways of checking boundary conditions (of inputs, outputs, and side effects).
Tests (should) give confidence that you don't have any unintended or unwanted behavior.
Tests (should) give confidence that you have intended and wanted behavior.

Tests give confidence that the code does what it's supposed to do.

I don't think it's any dispute that tests increase refactorability, unless they're bad tests.  A horrible test suite which anchors you to an implementation can decrease refactorability.  If my ability to make changes to a codebase is restricted by some poorly written tests, this is not a win.  But of course, tests can always be deleted, or rewritten.  So it's not as much of a loss as I'm making it out to be.

So do tests increase refactorability?  They can, if they describe what the code should do, and what it shouldn't.  This requires a skilled drafter of tests and a subject matter expert.  But it's a skill, and you can always get better at a skill with practice.

Is it worth it to learn how to write tests?  Well it will certainly win you brownie points in the corporate programming world, but this may be another personal choice you have to make.  But, once again, hopefully I've given you some tools to think about it and come to a reasoned and sound position.

Code Coverage

Code coverage is an interesting thing.  There are no official definitions, but there are two main notions.  One is an objective sense of coverage to mean that your test suite causes every line of code in your application to be used at least once.  An awesome tool for checking this is Coveralls.  It will even tell you how many times each line was grazed over by your tests.

But I don't think that's what Bob Martin means by coverage.  I think he means something a little more beefy.  Something like "all the possible common inputs were tested".  But this begs the question "What is a common input".  For example, if you write a square root function in a dynamically typed language, should you test what happens if someone asks for the square root of the string "hello"?  Would your test suite not have full coverage if you didn't?  It sounds silly, so to give him the benefit of the doubt, clearly full coverage requires checking all the reasonable inputs. So if you don't check if your square root function deals with negative input, we may say it doesn't have full coverage, but this can be a case-by-case basis.

But even if we should strive for coverage, it doesn't follow that tests should be written first, just that they should be written.  I'll let you decide on that though.

Tests Should Be Written Before the Code they Cover

 So I think this is one of the weaker points in the argument.  The argument for it is that if you write your tests later, how will you know every line was covered?  And I think tools like Coveralls do a wonderful job telling you what code you aren't testing.  Writing tests first might be a good habit, but it's not by any means necessary for the goal of refactorability, even if it is paramount.

So I leave you with some thoughts to ponder with your friends.  What do you think?  Have I missed any important points?  Have I misrepresented TDD so terribly I should go sit in a corner?  I love discussion, and I'd love to hear your thoughts.

Sunday, August 10, 2014

Object relationship between Device, Data, and Format

In the world of digital sound, data and format are invariably coupled.  You cannot understand a data stream without an interpretation, and the format is that interpretation.

In the Sound gem, we have Device, Format, and Data objects.  A Data has to have a Format for a Device to interpret it.

    data = Data.new
    data.format = Format.new
    device = Device.new
    device.interpret data

    class Device
      def interpret(data)
        #given data.format, read data
      end
    end

A Device has no format, but it interprets Data given one.