When it comes to programming I always think about the quality. And when we talk about quality, we need to mention testing. Testing software is very important. This is for me so natural, so transparent that almost obvious. I realized that this isn’t obvious to everyone on one of my presentations. I asked the audience about testing and I have already known, that I need to talk about testing more. When I write a code, I also write tests, but I do that in reverse order. First, I write a test, then the code. This are for me two elements in one process - software development. You can see that in normal life too. When you are preparing a dish, you always taste it before serves it to your guests. You want to be sure, that the dish is delicious before you show it. The same should be with applications. First we test application, then we show it to the users.
Manual tests are boring
You are programmer. You write a code, but how you can be sure that your code is working correctly? You need to test it. You can do it manually. Yes, it is possible, but each time when you change something in your code, you need to do the same steps for testing your code one more time. We often aren’t able to do manual testing exactly the same way. It is hard to remember all the steps. It is boring too. How long you can click the same functionality over and over again without feeling of tiredness? Trust me, not too long.
Working on project without automated tests
In one of my past projects where I was a programmer, we had something what was called as Friday Testing Session. This was a time, where all programmers in the team did manual testing of whole application. We had a spreadsheet with many test cases. So you went through this list and you manually checked if functionality is working. First time it was exciting. I had the opportunity to know application better. Second time it was OK. I could understand the process, the workflow of the app. Third time, well somehow I went through these tests. Later, each time when I was doing these tests, it was one big pain. Because the application wasn’t prepared for doing automatic tests, moving each manual test to automatic tests was very time consuming and very painful too. As you can guess without this manual tests we didn’t know if after some changes application is still working correctly or not. In this situation your code and your application aren’t agile. You cannot make changes quickly. When you do change you afraid that it can break something else. And this was pretty common, because of coupling in the code. Time, which was needed to push something on the production, was not only related to change itself, but also with manual testing. Each team member spent about 2h on testing the application. If you have 6 people in the team this is the same like 1.5 workdays for one person!
TDD - the way to have tests in your project
The situation where we were as a team in this project wasn’t colorful. So, let’s talk about thing we could do to avoid similar situations in the future. Let’s talk about TDD. Someone will say that TDD is not a way of doing tests, but the way of doing software development. That’s true, but testing is a part of this process. So, let’s start from the beginning.
What is TDD?
TDD - Test-Driven Development is software development driven by tests. That’s mean each time you would like to add more logic to your code, first you need to write a test. Then based on this test you check if your solution add the right value to your application. And because of this test, you can try different approaches, find simpler solutions or do safe refactoring. Your tests are your guards. That’s it. This is all you need to know to understand TDD process from the big perspective. Now you need to repeat the process, till you get the functionality you want.
Now the hardest part. How to implement TDD on daily bases? First, let’s split TDD process into smaller steps. We can see 3 steps of TDD.
We write a test which will check small increment (small logic modification). We run this test and it should fail. If at this point, test is passing, it is useless. New test should cover new behavior in code, when it is passing there is no new behavior needed. This is why the failing test is so important in this step.
We write a code which will pass the test. This should be a first, fast solution which comes to our mind. At this point we don’t care if this is the best solution. Our solution should make the test pass. That’s all.
Now is the time to rethink the solution. Can you improve something? Can you simplify something? This is the time to do it. But remember, all the tests must pass after your changes. In this step you only improve, what you already have. You do not add new functionality.
We have the basics, now let’s focus on example. We will focus on writing one method using TDD. This will be a method for a poker hand. We will try to check, if poker player has one color (the same suit) in the poker hand. We will not focus now on carts representation. Let’s say that color (suit) will be represented by numbers and poker hand will be represented by an array.
Step 1 - RED - Create first test
We start from the test:
require 'spec_helper' describe 'flush?' do it 'checks if array has one color' do flush_rule = flush?([1, 1, 1, 1]) expect(flush_rule).to eq(true) end end
This test will check, if we have one color in a poker hand. Let’s try to run it:
$ rspec spec/lib/flush_spec.rb Randomized with seed 35317 F Failures: 1) flush? checks if array has one color Failure/Error: flush_rule = flush?([1, 1, 1, 1]) NoMethodError: undefined method `flush?' for #<RSpec::ExampleGroups::Flush:0x00000002a73d50> # ./spec/lib/flush_spec.rb:5:in `block (2 levels) in <top (required)>' Finished in 0.01294 seconds (files took 0.75094 seconds to load) 1 example, 1 failure
Our tests failed. We finished the first step of TDD. Now we can check, why the test is failing and try to fix that.
Step 2 - GREEN - Make code pass test
We see message
undefined method flush?. So, we provide exactly, what the test is asking for. We create a method.
def flush? end
We run the test again.
$ rspec spec/lib/flush_spec.rb Randomized with seed 28476 F Failures: 1) flush? checks if array has one color Failure/Error: def flush? end ArgumentError: wrong number of arguments (given 1, expected 0) # ./spec/lib/flush_spec.rb:3:in `flush?' # ./spec/lib/flush_spec.rb:9:in `block (2 levels) in <top (required)>' Finished in 0.01024 seconds (files took 0.66129 seconds to load) 1 example, 1 failure
It’s still not working. Now we get a message:
wrong number of arguments (given 1, expected 0). We need to add an argument to our method.
def flush?(array) end
Run the test again.
$ rspec spec/lib/flush_spec.rb Randomized with seed 34173 F Failures: 1) flush? checks if array has one color Failure/Error: expect(flush_rule).to eq(true) expected: true got: nil (compared using ==) # ./spec/lib/flush_spec.rb:10:in `block (2 levels) in <top (required)>' Finished in 0.05983 seconds (files took 0.83267 seconds to load) 1 example, 1 failure
The test is still failing, but we are closer to the solution. This time we see method was running, but we expected to get a
true not a
nil. This can be easily fixed. We need to return
true from our
def flush?(array) true end
We run the test one more time.
$ rspec spec/lib/flush_spec.rb Randomized with seed 40116 . Finished in 0.01189 seconds (files took 0.65796 seconds to load) 1 example, 0 failures
Our test pass. This green dot finish our step 2. Yes, I know this is not yet the functionality we want to implement. We will get there, be patient. At this moment you can ask: Why we need to do so small steps? Why we cannot just write this simple method? We can. The size of the step depends on you. If you feel confident that you know exactly what to do, you can write the implementation for passing the test as a one bigger step. However, in most cases we just think we know what to do, but there is some uncertainty. When after writing a code test is not passed, probably this is a good sign to make smaller steps. This will allow you to get to the working code and to have confident that it is working in a right way.
Step 3 - REFACTOR
As you see, it is very simple code and logic. So probably when I will tell you, that there is a duplication, you will not believe me. If you think about a duplication in most cases you will just think about the code. Here the duplication is between code and test. In both cases we have the
true value. It is not big duplication, but it exists. At that point we could remove this duplication. To do that we could use the approach from chapter 1 in Kent Beck book TDD - By example. This will give as more abstraction for our code, but with only one test. In this case this will be a final implementation. If you feel OK with having only one test and the final implementation, you can try it. If you are more like me, then let’s go and prepare one more test.
Why this test is important for me? Well, at that moment I don’t feel comfortable enough to change the code having only one test. This is for me too big step. To take care of this code I will use triangulation approach.
What is the triangulation?
In the Math world triangulation is a ways of finding the location of a point by using triangles with known points. In our case the known points are the test cases and the location which we search for is the real logic implementation. So on crossing the test cases, we will find the final method.
Step 1 - RED - Create second test
So, to use triangulation, I will write another test which will check the behavior of
flush? method for another condition. Right now we check the behavior, when we have only one color in the poker hand. It’s time for checking what will happen when we have more than one color. To do that, we go back to our TDD circle, the first step.
describe 'flush?' do it 'checks if array has one color' it 'checks if array has more then one color' do flush_rule = flush?([1, 1, 2, 1]) expect(flush_rule).to eq(false) end end
This time we check if our method will return
false for more than one color.
$ rspec spec/lib/flush_spec.rb Randomized with seed 6606 .F Failures: 1) flush? checks if array has more then one color Failure/Error: expect(flush_rule).to eq(false) expected: false got: true (compared using ==) # ./spec/lib/flush_spec.rb:15:in `block (2 levels) in <top (required)>' Finished in 0.04907 seconds (files took 0.63654 seconds to load) 2 examples, 1 failure
We run tests and we see that one test is failing, so we can go to the second step of TDD.
Step 2 - GREEN - Implement the real logic
We see that expectation for second test is different. We return
true, but for more than one color it should be
false. Now we can think about the real solution. We can check how many unique colors are in the array. If there is only one unique color, then we will return
true in other case we will return
false. This is exactly, what the code below do.
def flush?(array) array.uniq.size == 1 end
We run tests one more time and we see that they are passing.
$ rspec spec/lib/flush_spec.rb Randomized with seed 33907 .. Finished in 0.01092 seconds (files took 0.57891 seconds to load) 2 examples, 0 failures
Step 3 - REFACTOR
We can try to refactor this code, but in our example code is so simple, that I think it is enough to leave the method like it is right now. We don’t have the context, so it will be hard to decide, what is the best next step for the refactoring. If you like, you can always do small simplification like:
def flush?(array) array.uniq.one? end
I would like to show you one more thing. Because the code simple and because we write this code in Ruby programming language, we can use this method in many ways. We can use it like we did before, just for a simple number representation of colors.
flush?([1, 2, 1]) # => false
We can also use this
flush? method with the real colors declared, for example by string representation.
flush?(['#fff', '#fff', '#fff']) # => true
With little effort we can do the same for object representation of colors. This is the beauty of simple code. Possibility to use it many times in not a trivial way.
We went through the TDD process. We saw how it looks. Now is the time to summarize what this TDD process give us.
Flow - This is one of the most important feeling during the creative process. I think programming is a creative process. To go into flow we need to focus on one thing at one time. TDD helps us with that. You don’t need to over thinking. You just go where your tests are taking you. Step by step you know what to do next. You can also adjust the size of the step for your needs. Sometimes steps will be bigger and sometimes smaller.
Assurance - Quality assurance. We want to be pretty sure that we cover all edge cases. We want to be sure that code is doing exactly what it should do.
Fearless refactoring - If we have enough tests which are our guards, we can easily do the refactoring without stress. We know that we’ll not break our logic.
Experiment friendly environment - you can test a new approach to the problem with fast feedback loop. If something failed, you know that almost immediately.
Continuous progress - Even if you go slow, this is still progress. You don’t need to think about whole problem. You focus only on a small part of it. Step by step you are closer to a solution. If you see progress, you also feel more motivation. You also do less bugs during the development process, because you are secure with your tests. This also speeds up your development process. You don’t need to stop on the bug and fix it.
Communication - Our tests can be some kind of documentation of our logic. The living documentation, not the obsolete one.
In the end my last thought. If you read this and say: “Well, my project is different. It’s impossible to use TDD there.” Try to open your mind and break this negative thinking loop. If you feel that you cannot use TDD right now in your project, start small. Take some simple example of logic: Tic Tac Toe, Game of life or some kata like Gilded Rose Kata. You can learn on these examples, how to work with TDD. Do small steps. When you get used to TDD, then go back to your real application and start from small steps doing TDD. It can really change your thinking and your work conditions. This is a long term plan, like savings for retirement.
The most common Ruby Testing Tools
- Minitest - A complete suite of testing, supporting TDD, BDD, mocking and benchmarking
- RSpec - BDD tool for Ruby, but you can use it just for TDD
- Capybara - Acceptance test framework for web applications
- Cucumber - Tool for running automated tests written in plain language
- Working Effectively with Legacy Code - Michael Feathers
- Test Driven Development: By Example - Ken Beck
- The Pragmatic Programmer: Journey to Mastery - Andrew Hund, David Thomas
- Clean Code: A Handbook of Agile Software Craftsmanship - Robert C. Martin
- Design Patterns in Ruby - Russ Olsen
- Practical Object-Oriented Design in Ruby: An Agile Primer - Sandi Metz
- All the Little Things by Sandi Metz
- Nothing is something by Sandi Metz
- 8 best Ruby on Rails refactoring talks
- TDD - Back to the Future
- Are you an Egoistic Programmer? - presentation about refactoring