Why I want to introduce mutation testing to the railseventstore gem
… and check why 5600+ Rails engineers read also this
Why I want to introduce mutation testing to the rails_event_store gem
We have recently released the RailsEventStore project. Its goal is to make it easier for Rails developers to introduce events into their applications.
During the development we try to do TDD and have a good test coverage. The traditional test coverage tools have some limitations, though. Mutation testing is a different approach. In this post I’d like to highlight why using mutation testing may be a good choice.
Let me start with one example. In this example, mutant discovers uncovered code. Other tools think this code is well-covered.
In the RailsEventStore (RES) implementation, we use the concept of a Broker. The broker allows subscribing to certain kinds of events. As part of the subscription we pass the subscriber object. In the current implementation, we expect that such a subscriber has a handle_event
method.
module RailsEventStore
module PubSub
class Broker
def initialize
@subscribers = {}
end
def add_subscriber(subscriber, event_types)
raise SubscriberNotExist if subscriber.nil?
raise MethodNotDefined unless subscriber.methods.include? :handle_event
subscribe(subscriber, [*event_types])
end
end
end
end
When the raise MethodNotDefined unless subscriber.methods.include? :handle_event
line was introduced it didn’t come with any test. Despite this fact, the coverage tools assume it does have a coverage. That’s because the previous tests do go through this line and consider it covered.
Let’s turn this line into this:
if !subscriber.methods.include? :handle_event
raise MethodNotDefined
end
With this code, the simple coverage tools are able to detect that the if
block is never executed.
As you see, the coverage metric is now depending on the fact how you format the code. That’s not good.
If we run the first piece of code with mutant, it does detect that there is a lacking coverage.
Mutant output
When I run mutant on RES, I use the following:
bundle exec mutant —include lib —require rails_event_store —use rspec “RailsEventStore*”
which results in the following summary:
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[<Mutant::Expression: RailsEventStore*>] subject_ignores=[] subject_selects=[]>
Integration: rspec
Expect Coverage: 100.00%
Jobs: 4
Includes: [“lib”]
Requires: [“rails_event_store”]
Subjects: 47
Mutations: 1238
Kills: 888
Alive: 350
Runtime: 108.15s
Killtime: 159.60s
Overhead: -32.24%
Coverage: 71.73%
Expected: 100.00%
It’s worth noting that mutation testing is very time-consuming. In our case, the time spent was the following:
real 1m52.906s
user 3m56.833s
sys 2m34.144s
My goal is to setup Travis to run the mutation tests on every push. Also, I’d like to set up a 100% expected mutation coverage in the future.
This is an example output of the mutant run on a Travis machine. It’s worth looking at, as you can see the full output. Mutant shows us every alive mutation - the ones that don’t break tests. One example:
def version_incorrect?(stream_name, expected_version)
unless expected_version.nil?
- find_last_event_version(stream_name) != expected_version
+ find_last_event_version(stream_name) != nil
end
end
This output means, that our coverage here was not perfect. Simply replacing the expected_version
with nil
is still passing all tests. That’s not good. We can’t really rely on our tests if we want to refactor this code.
What’s wrong with the lack of coverage?
The problem with lacking test/mutation coverage is that it’s easy to break things when you do any code transformations. The RailsEventStore is at the moment changing very often. We try to apply it to different Rails applications. This way we see, what can be improved. The code changes to reflect that. If we have features without some tests, then we may break them without knowing it.
My goal here is to have every important feature to be well-covered. This way, the users of RES can rely on our code.
Feel free to start using RailsEventStore in your Rails apps. We already plugged it into several of our applications and it works correctly. In case of any question, please jump to our gitter channel, we’ll be happy to help.