Real-World TCR

Thomas Deniffel
8 min readFeb 21, 2019

--

When you read about TCR, you always get the Fibonacci-Example or — if you are lucky — something slightly bigger. But you don’t find any “real-world” examples. This article tries to fill this gap. It provides an example of a web-app with a web-API for customer and order management done with TCR.

tl;dr TCR is not exciting, but useful as it will outsource your discipline. Real-World projects are possible without much more effort than TDD through the techniques we already know from TDD.

This tutorial is optimized for showing TCR and not to deliver a useful product. Therefore the code quality and structure suffer. But they should be easy to refactor (the test coverage is there through TCR).

I’ve split it into several parts. This article is part one. I will create part two to n as soon as I gathered enough feedback.

Note: I suggest you follow this tutorial on your machine after you cloned my repo: https://github.com/tom-010/tcr_webapp. I will mention different checkouts where you can sync up with this article.

Getting Started

Here the structure of the core-classes:

We will handle the following Use-Cases:

ATTENTION: If the steps become boring, skip to the conclusion!

Setting Up TCR

I will create this app in Java with Spring Boot only because this is the fastest for me. I am sure; you can reproduce this quickly in any language/framework. Nothing special happens here. I will explain the very few annotations so that a non-Java-Programmer can easily follow.

Note: If you not quite sure, what TCR is, check out this post.

I choose the TCR Variant ‘The watch buddy’ together with ‘BTCR’:

$ tree .
.
├── scripts
│ ├── buildIt.sh
│ ├── commit.sh
│ ├── revert.sh
│ └── test.sh
├── tcr.sh
└── watch.sh
$ cat watch.sh
while true
do
inotifywait -r -e modify ./src
./tcr.sh
done
$ cat tcr.sh
./scripts/buildIt.sh && \
(./scripts/test.sh && ./scripts/commit.sh || ./scripts/reset.sh)
$ cat scripts/buildIt.sh
./gradlew build -x test
$ cat scripts/commit.sh
git commit -am working
$ cat scripts/revert.sh
git checkout HEAD — src/main/
$ cat scripts/test.sh
./gradlew test

The watch buddy waits for changes and executes the “TCR”-script as soon as you hit save (you have to install “inotifywait”). TCR does the common TCR or better said the BTCR: build && (test && commit || revert). This does not revert your project if you have a typo (really really useful in a real-world-project, where you have to solve real tasks).

Next, the single steps are split into scripts. I’ve found this useful in the past if you want to adjust the individual parts. For example, the build can become more sophisticated, ‘test’ can run different test-types and you may want to tweak the TCR-Variant so that it fits your need. I did this: The revert script reverts the src-directory. This solves the vanishing test-problem. The vanishing test problem means that a red test just gets reverted as soon as you finished it. There are ways to solve it, but I’ve found this approach most useful.

Spring Boot

Next, I created a simple Spring Boot Project via Spring Initializr. The directory looks now like this:

$ tree .
.
├── build.gradle
├── gradle
│ └── ...
├── gradlew
├── scripts
│ ├── ...
├── settings.gradle
├── src
│ ├── main
│ │ ├── ...
│ └── test
│ └── ..
├── tcr.sh
└── watch.sh

You can ignore everything except for “src/main” and “src/test”. You can test your application via: “./gradlew test”, build it via “./gradlew build” and run it via “./gradlew bootRun”.

You now can try to run TCR:

$ ./watch.sh

You can also come to my state with (if you cloned the repo):

$ git checkout 4f868b1cbeea612f179e58544c520dc585fa9adc

First Tests — London Style

Rest-Controllers are just normal classes together with some annotations (added later) in Spring Boot. It would be best practice to test the resulting JSON of the controllers for the API-Tests. Testing the Java-Classes is simpler regarding the tools necessary.

UC: Create Customer

$ open src/test/java/io/deniffel/tcr/CustomerAPITest.java

I add the first Test:

package io.deniffel.tcr;

import org.junit.Test;

public class CustomerAPITests {

@Test(expected = IllegalArgumentException.class)
public void creatingCustomerWithNulnew LinkedList<Object>();l_Throws() {
new CustomerApi().create(null);
}
}

To fix this, I create the CustomerApi.

$ open open src/main/java/io/deniffel/tcr/CustomerAPI.java

With:

package io.deniffel.tcr;

public class CustomerAPI {

public void create(Object o) {
throw new IllegalArgumentException();
}
}

Thanks to the “build &&” TCR does not revert my code while I use the auto-generated functions of IntelliJ because the project did not build until I had to code above.

Now I am green and can take the next step. We need an “allCustomers” endpoint for other tests:

@Test
public void allCustomers_initialNoElements() {
assertEquals(0, new CustomerAPI().all().size());
}

It does not compile. No commit or revert is executed until:

public class CustomerAPI {
// ...

public List<Object> all() {
return new LinkedList<Object>();
}
}

Now we will add some real logic:

@Test
public void allCustomers_correctListSize() {
CustomerAPI api = new CustomerAPI();
api.customers.add(new Object());
api.customers.add(new Object());
api.customers.add(new Object());

assertEquals(3, api.all().size());
}

This has to be fixed before the code compiles. The good news. It is quite easy:

public class CustomerAPI {

List<Object> customers = new LinkedList<>(); // 2. change!

public void create(Object o) {
throw new IllegalArgumentException();
}

public List<Object> all() {
return customers; // 1. change!
}
}

Time to use our refactoring hat and introduce a Customer class:

package io.deniffel.tcr;

public class Customer {
}

And firstly refactor the prod-code, then the test-code.

public class CustomerAPI {

List<Customer> customers = new LinkedList<>();

public void create(Customer o) {
throw new IllegalArgumentException();
}

public List<Customer> all() {
return customers;
}
}
public class CustomerAPITests {

@Test(expected = IllegalArgumentException.class)
public void creatingCustomerWithNull_Throws() {
new CustomerAPI().create(null);
}

@Test
public void allCustomers_initialNoElements() {
assertEquals(0, new CustomerAPI().all().size());
}

@Test
public void allCustomers_correctListSize() {
CustomerAPI api = new CustomerAPI();
api.customers.add(new Customer());
api.customers.add(new Customer());
api.customers.add(new Customer());

assertEquals(3, api.all().size());
}
}

Quite simple. The “new Customer()” bugs me, therefore after an “Extract Method”

@Test
public void allCustomers_correctListSize() {
CustomerAPI api = new CustomerAPI();
api.customers.add(createValidCustomer());
api.customers.add(createValidCustomer());
api.customers.add(createValidCustomer());

assertEquals(3, api.all().size());
}

private Customer createValidCustomer() {
return new Customer();
}

Let’s continue with our use-case:

@Test
public void createValidCustomer_itIsListed() {
CustomerAPI api = new CustomerAPI();
int sizeBefore = api.all().size();
api.create(createValidCustomer());
assertEquals(sizeBefore + 1, api.all().size());
}

Sure enough. The tests fail as we addCustomer just throws an exception. We only have one shot with TCR to fix this. This is good as we are forced to do very tiny steps. This step is tiny:

public void create(Customer c) {
if(c == null)
throw new IllegalArgumentException();
customers.add(c);
}

Great. From the API-Level, this works. I could now create a Repository and a Database and a Database-Connector and so on. But I don’t see much sense in this. Rather continue with a little bit of Validation.

Validation

Normally I use the validation provided by Spring, but I am aware that probably most of the readers are not using the same. Therefore I stick with vanilla Java.

public class CustomerValidationTest {

@Test
public void emptyCustomer_notValid() {
assertFalse(new Customer().isValid());
}
}

This does not compile. Let’s make it green.

public class Customer {
public boolean isValid() {
return false;
}
}

No automatic reverting: I made it!

@Test
public void customerWithName_valid() {
Customer c = new Customer();
c.setName("name");
assertTrue(c.isValid());
}

Build does not work. I have time to extend the customer:

public class Customer {

private String name;

public boolean isValid() {
return name != null;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

This was the trick. Firstly, I had “==” instead of “!=”. TCR reverted this. It is like a pair-programmer, that immediately points to an error. Nice: Working without thinking.

Emergency Button

Now a test-case for documentation:

@Test
public void customerWithoutName_notValid() {
Customer c = new Customer();
c.setName(null);
assertFalse(c.isValid());
}

It is still correct. Nothing to do. But, what would happen, if we had an error in a test-code. I simulate this with:

@Test
public void customerWithoutName_notValid() {
Customer c = new Customer();
c.setName(null);
assertTrue(c.isValid());
}

We are now in a red state and TCR (or better BTCR) is not able to bring us out of this green state. Worse, because we have our “watch” running, we don’t even check the test results.

But we will recognize that we are red as soon as we type some kind of production-code. It will immediately revert because of the wrong red test case.

For this case, we create an emergency button: “revertAll.sh”

$ cat ./revertAll.shgit reset --hard

Our test was automatically reverted to a green state:

@Test
public void customerWithoutName_notValid() {
Customer c = new Customer();
c.setName(null);
assertFalse(c.isValid());
}

To sync up with me:

$ git checkout dd912c8928ebd33102ca136f05d269cc0672b30e

End of Part 1 — Conclusion

I expect that the majority of my readers skipped here after the first view code examples because of the steps were obvious and kind of boring. I am quite happy about this!

The tasks (customer management) itself is not very exciting. TCR should be a useful tool and not exciting for itself. The fact, that you can do a boring real-world-project using TCR without introducing exciting problems, shows the readiness for day-to-day projects.

This ability to be boring is not true for every new cool technique. I could have done the example above with microservices. They would make this task very exciting: Distributed System, Self-Healing, Ci-Pipelines, Muti-Repo, ….
All these stuff would be a hell of fun, but they would not provide value.

After using TCR in “real-world projects” (I hate to repeat this phrase over and over again, but I don’t know another) it is not exciting anymore but I also do not want to miss it for many tasks.

To apply it successfully, you need a TDD strategy, like the London-Style or Chicago-Style as well as some TDD techniques like triangulation. You use them normally anyway when applying TDD — TCR just enforces them. This sounds restrictive first, but as soon as you got used to it, you do them automatically and applying them becomes easier. Because you outsourced the required discipline.

This is, what TCR is for me: TDD with outsourced discipline. In the TDD-Community, you will often hear it. I predict you will not hear it the TCR community. It is done by a tool.

Without discipline, brainless coding becomes possible. Not for the difficult tasks, but for the easy ones. Brainless coding does not mean, that you don’t have to think. It means that you can focus on the domain, not the code.

In short: TCR is not exciting, but useful as it will outsource your discipline. Real-World projects are possible without much more effort than TDD through the techniques we already know from TDD.

--

--

Thomas Deniffel

Programmer, CTO at Skytala GmbH, Software Craftsman, DDD, Passion for Technology