Skip navigation.

Notes to self

Whatever I feel like writing

Posts tagged with "tdd"

Video links

, , ,

Four Kinds of Tests for Basic Correctness

, ,

This is from J. B. Rainsbergers talk Integrations Tests Are a Scam, which by the way is a great talk that is very worth watching.

Rainsberger defines three kinds of tests:
  1. Collaboration tests
  2. Contract tests
  3. State-based tests

I would like to add a fourth kind to that list, called the Concurrency tests.

Let me try to explain what each of those are about, in turn.

Collaboration tests: I talk to a bunch of other guys and, assuming they work correctly, am I working correctly? In essence, we are testing the correct behaviors of the individual units in our system, under the assumption that all other units work correctly in accord with their contract.

Contract tests: someone uses this thing in a certain way and expects a certain behavior from that, do you live up to that contract? In essence, we are testing that the units use each other correctly. In other words, that we have gotten the links between the collaborators right.

For example, if we have ThisThing that expects ThatThing to return an empty list under certain conditions, then we would mock the world as such in our collaboration tests but we will make sure that this assumption holds, in our contract test. In other words, we must have a test that sets up ThatThing in those certain conditions and verifies that it really does return and empty list, and not something else, like null or a list with something in it.

State-based tests: doing someAction to someThing must produce thisObservableEffect. These are the good old super simple tests for pure functions, or very simple state mutations, or side-effects to the outer world.

Concurrency tests: like state-based tests, except the observable effect is some specific behavior of the unit under concurrent use.

Concurrency tests makes sense to separate from state-based tests because a) things that deal with concurrency should deal with nothing else, and b) testing for specific concurrent behaviors is typically harder and more complex than single-threaded state-based tests and if they are not then you are doing something horribly wrong with your state-based tests.

If you do these tests, and design your system this way with all these things ind mind, then, Rainsberger postulates, you will only need integration tests as a debugging tool for figuring out which contract or collaboration tests are either missing or wrong. And once that issue has been fixed, you will have a focused object test that covers the same exact case as the integration test did and so you remove the latter to get rid of the duplication.

I must say that that sounds like a very nice place to be.

Thoughts on Corey Haines talk with JBrains

,

This post is just to collect my thoughts on Corey Haines' talk with J. B. Rainsberger on moving specificity towards the test.

Rainsberger presents these four elements of simple design:

  • Passes test
  • Minimizes duplication
  • Maximizes clarity
  • Is small

If we are doing TDD as Uncle Bob prescribes, in very short red/green/refactor cycles, then the "passes test" part becomes a non-issue and goes away. Then, the "is small" goes away because any body of code that has minimized duplication and maximized clarity is unlikely to be unnecessarily large.

This leaves us with these two takeaways from the talk:

  • Focus on removing duplication leads to well structured code.
  • Focus on bad names and clarity leads to a good distribution of responsibility.

Removing duplication is not just code, but also intent. Say and Inventory knows how to format prices, while some controller object knows how to write "No price available" if the Inventory could not find a price for some product. Both of these deal with how to present a price, but they deal with different parts of the presenting. So there is no duplication in the code, but there is duplication in the intent. Both of these pieces of functionality could moved to a view or presentation layer, and then combined into a whole. This removes the duplication of intent.

It sounds like an application of the single responsibility principle, and that's not a bad thing.

As part of the process of removing duplication, one can start by moving the duplicated parts closer to each other. Say we have a Catalog object that knows how to turn barcodes into prices. If I have a test where I pass in a certain barcode and expect to get back the price 12, I could initially just hard-code the number 12 into the Catalog. As I test for more and more barcodes, this turns into a lookup table. Then we can notice the duplication of the hard coded values. If the lookup table is really a map underneath, I can move it to the test and then pass it into the Catalog through the constructor.

This in turn leads the code to naturally evolve toward a model-view-control design. The concerns needs to be separated so they can be passed in and stubbed if need be.

Bad names and clarity is a bit harder to grasp. Frankly, the way he describes things makes it hard to see design effects are caused by removing duplication and which are caused by gaining clarity. So it ends up getting mixed together in his explanation. To me, the structure of the code and the distribution of responsibility are two sides of the same coin. It can be that he meant to say that this is the way it should be. His example of the MVC structured code where the intent and logic of formating prices is duplicated and located in multiple different places, seems to support this conclusion.

Code Need

, , ,

The following is something I intend to sleep on:

Introduce abstractions out of need. Abstractions that are introduced through design rather than need are inherently over-designed.

Programming is about satisfying need.

Agility is about reacting to need rather than anticipating it.

TDD is a process of formulating need at a low level.

Breaking the Law (of TDD)

, ,

The Three Laws of TDD is an excellent and productive method of raising the quality-bar of your code.

This "proper" form of TDD ensures a number of things. First, your code is testable. If it is not, your tests will become too hard to write, but once you have a good coverage, you can then safely refactor your code such that it becomes testable. This is a good thing because testability is a property of design that implies modularity, compose-ability and flexibility.

Second, the closed think-test-code-refactor loop makes your programming intentional and deliberate, as oppose to programming by coincidence. It forces you to think about your code and your design before you get typing. I believe that these extra brain cycles aren't just wasted on trying to figure out how to write a test of non-existing code, but also partially translates into code that is better thought through and, ultimately, better written.

Lastly, the most direct effect of this process is the test coverage gained. If followed strictly, then you are in theory guaranteed that all lines in your code are covered by tests. In reality however, the dreaded 100% coverage is not only overrated, but can lul you into a false sense of security. But, proper test coverage enables refactoring and thus makes it possible to continuously improve the code base.

All nice and well. I try really hard to adhere to the laws of TDD, but there are things that you just can't test. And not just the usability, but code-things too. This is especially true for highly concurrent code where failures are probabilistic and sometimes even impossible under certain conditions (hardware, VMs or compiler flags). I have sort of accepted this property of concurrent code, because I find that I can often encapsulate the concurrent parts and make them simple enough to be verified analytically.

However, the other day, I ran into a problem. I needed to write a function. It did not modify any state, was idempotent, did not involve any concurrency (the whole program was single-threaded) and it did not even have anything to do with usability. But I could not write a test for it.

The purpose of this function was to return the absolute path to the current users home directory. That was all. How do I test that? I could hard-code the path to my own home directory in the test, but then it would fail for everyone else. I could try the function manually and check the result myself, but I don't have a windows machine and this program was suppose to be platform independent. Still, that was what I ended up doing. Then I just copy-pasted some code from somewhere to fill in the windows special case and hope for the best.

It felt like a dirty thing to do, because this program had been written with TDD from the start, and I had just written the first function without any unit tests. Indeed, parts of it wouldn't even run on my machine because it was windows specific.

I won't make it a habit, though. I need to keep telling myself that.