Note: Welcome everyone, thank you for having me here.
Software Craftsperson
Software Engineer
@ Pivotal Labs
Note: My name is Oleksii Fedorov. I am a Software Craftsperson and this is my twitter handle. I work as a Software Engineer @ Pivotal Labs.
- Increase test coverage
- Increase understanding of the code
- Test-drive your tests
Note: Today you will learn how to eliminate fear of changing legacy code. You will learn how to confidently and iteratively understand legacy code better and increase test coverage in the process. While code examples will be in Ruby, the technique is language-agnostic.
- Hard to understand
- No tests
- Brings value
Note: For the purposes of this talk I need to define what Legacy Code means. It is hard to understand. It has no tests or almost no tests and it brings value to the business and customers.
Let's take a look at what we will be going through today:
- Knowledge in Code
- Mutation
- Code <-> Test Relationship
- Most Useful Coverage Metric
- Mutational Testing
- Explorative TDD
- Step-by-step Example
- Outside of Legacy Code
Note: We will define what Knowledge in the ProdCode and Mutation means. We will take a look at the relationship of the ProdCode and TestSuite. Then we will see what is the most useful coverage metric is. Then we will explore Mutational Testing and Explorative TDD techniques. And we will go through the example. Shall we get started?
Note: I think you get the idea. And if we change small piece of knowledge, we are introducing...
Mutation - granular change of knowledge, that changes behavior of the system.
Note: ..Explain what this code does..
So let's pick a first bit of knowledge here:
Note:
Other available mutations here: Changing if
condition to always be false
.
Note:
Inverting if
condition.
Note:
Commenting out if
body
Note:
Commenting out else
body
- Makes sure Code is correct
- Enables refactoring
- Gives courage to introduce change
- Coupled to Code
- Knowledge should be verified by Test Suite
- Mutation should lead to a Test failure
- Knowledge in Code
- Mutation
- Code <-> Test Relationship
- Most Useful Coverage Metric
- Mutational Testing
- Explorative TDD
- Step-by-step Example
- Outside of Legacy Code
- Narrow scope to single granular piece of knowledge
- Break this knowledge (simple change, Mutation)
- Make sure there is a test suite failure
- Restore knowledge to the original state
- Narrow scope and isolate it
- Read, try to understand, pick granular piece of knowledge
- Write a test
- Make sure test passes
- Apply Mutational Testing repeatedly
- Go back to 2
- Knowledge in Code
- Mutation
- Code <-> Test Relationship
- Most Useful Coverage Metric
- Mutational Testing
- Explorative TDD
- Step-by-step Example
- Outside of Legacy Code
- Extract completely to module/class/package of its own.
- Duplicate the code under the test and put it into function/method(s) with different distinguishable name.
- change our understanding if it is not what we expect, or
- change our test to cover that, or
- add more tests.
- verify that test suite is still correct after refactoring
- verify that test suite is not rigid (and identify parts requiring refactoring)
- Narrow scope and isolate it
- Read, try to understand, pick granular piece of knowledge
- Write a test
- Make sure test passes
- Apply Mutational Testing repeatedly
- Go back to 2
Note: ..Great point to stop, give audience chance to ask questions and drink some water..
Introduce a mutation.
Note: Introduce a very small change to the knowledge. The test suite should fail. If it doesn't - knowledge is not covered well enough. And that leads us to the term called...
Note: Semantic Test Stability. Test Suite can be considered semantically stable if for any mutation to any bit of knowledge it tests there is a failing test. There are techniques that allow us to keep this metric up high. One of them is...
Note: Let's see it in action.
Note: First, we need to narrow our scope to a single bit of knowledge.
Note: Second, we need to introduce a mutation:
Note: Third, we need to make sure there is a test failure:
Finished in 0.02343 seconds (files took 0.11584 seconds to load) 4 examples, 0 failures
Note: Oh no, it didn't fail, so we have a "failing test" for our test suite. In this case we need to add the test for the negative case:
Finished in 0.02343 seconds (files took 0.11584 seconds to load) 5 examples, 1 failure
Finished in 0.02343 seconds (files took 0.11584 seconds to load) 5 examples, 0 failures
Note: Usually, to accomplish any useful behavior we would like to combine multiple bits of knowledge. So if we want to understand how system works better, we need to focus on groups of pieces of knowledge. This is what Explorative TDD is about:
The technique used to increase code coverage and understanding of the knowledge in the code.
1. Narrow scope to some manageable knowledge and isolate it
Note: (manageable knowledge = method/function/class/module)
2. Read and try to understand one piece of knowledge
3. Write a test to verify this assumption
4. Make sure it passes
Note: by altering the assumption, or fixing production code (bugs)
5. Apply Mutational Testing repeatedly
Note: Apply Mutational Testing to each related granular piece of knowledge to verify that the understanding (and the test) is correct (this may introduce more tests)
6. Go back to 2
NOTE: I think this is a good time to have some questions... I think we should go through a small example...
(step-by-step example)
Means of isolation:
Note: As you might guess, this code is really overwhelming. So, let's start by duplicating the whole method in an isolated method to test it:
def notifications_isolated notifications = Database .where("notifications") do |x| ... end
Note: Next we need to read the code and try to understand a concrete part of the behavior it has. We need to identify related knowledge for this behavior as a whole:
Note: These bits of knowledge indeed look related, so let's try to guess what behavior they are responsible for:
Note: Wait. I think we are making to big assumption. There is a smaller assumption that we need to validate here:
Note: Next step is to make sure that this test passes:
Finished in 0.02343 seconds (files took 0.11584 seconds to load) 1 example, 0 failures
Note: Now we need to apply mutational testing repeatedly to these bits of knowledge until we are confident that it is well-tested. For example:
Note: Let's take a closer look at this boolean condition:
Note: For example, We could remove it:
Finished in 0.03511 seconds (files took 0.11877 seconds to load) 1 example, 1 failure
Note: So it fails, great! Let's take a detailed look at the failure itself:
expected: 1
got: 0
(compared using ==)
# ./lemon_spec.rb:63
<span class="presented-code__highlight">expected: 1</span>
got: 0
(compared using ==)
# ./lemon_spec.rb:63
<span class="presented-code__highlight">expected: 1</span>
<span class="presented-code__highlight failure">got: 0</span>
(compared using ==)
# ./lemon_spec.rb:63
Note: This mutant is not surviving, which means that our test is good. Or is it? Let's see what other mutations we can introduce for this bit of knowledge:
Note:
Replacing condition with true
results in:
Finished in 0.02343 seconds (files took 0.11584 seconds to load) 1 example, 0 failures
Now we have to either:
Note: In this case, adding another test does the job:
Finished in 0.21531 seconds (files took 0.11582 seconds to load) 2 examples, 1 failure
expected: 0
got: 1
(compared using ==)
# ./lemon_spec.rb:131
<span class="presented-code__highlight">expected: 0</span>
got: 1
(compared using ==)
# ./lemon_spec.rb:131
<span class="presented-code__highlight">expected: 0</span>
<span class="presented-code__highlight failure">got: 1</span>
(compared using ==)
# ./lemon_spec.rb:131
Note: Great, it means that this mutation is covered by our tests too. It is important to undo the mutation and see all tests pass:
Finished in 0.02343 seconds (files took 0.11584 seconds to load) 2 examples, 0 failures
Finished in 0.02343 seconds (files took 0.11584 seconds to load) 2 examples, 0 failures
Note: It seems we need another test:
Finished in 0.14636 seconds (files took 0.12127 seconds to load) 3 examples, 1 failure
NoMethodError: undefined method `[]' for nil:NilClass
NoMethodError: undefined method `[]' for nil:NilClass
NoMethodError: undefined method `[]' for nil:NilClass
Note: And this failure is pointing to the code in the test:
Note: We made slightly wrong assumption about what will happen after the mutation and the failing test has proven us wrong. We had to investigate what really has happened and therefore we have deepened our understanding of this knowledge in the code.
Finished in 0.14636 seconds (files took 0.12127 seconds to load) 3 examples, 0 failures
(until there is enough confidence)
Choose new group of bits of knowledge responsible for other behaviors of the code.
And repeat.
(until there is enough confidence)
This step-by-step example can be viewed as commit history here:
Note: This concludes our example and I think it is time for questions... With that done, we should see, if that technique can be used outside of the context of Legacy Code...
Useful during big refactorings
(extract class/module/package)
Useful when refactoring tests
(rigid = one change -> 70% tests fail)
Note: Let's recap the technique itself and draw a bottom line.
Note: It is time for questions now.
Twitter: twitter.com/waterlink000
Github: github.com/waterlink
Blog: tddfellow.com