In this video, I'm just going to show you
a quick example of using code coverage driven by a unit test to review some code.
I'm about to do some refactoring.
I want to look at the code, see was covered, see was not.
Make some decisions about it.
And I use the code coverage to help me.
So all the code that we're seeing here is on GitHub.
It's part of my Challenger project.
There'll be a link in the description.
So let's just have a quick look at the test.
So I have a test here
and the test is using mocking because I'm working at an External Interface Layer,
which is the HTTP requests that are coming in.
And I'm not comfortable with the way that
I'm doing this so I'm about to refactor the code so that I don't have to do the
mocking here - and I'm working with internal classes.
But I've got some coverage here and I just
want to have a quick review of the code to see what is covered.
So the code that is actually being tested is over here.
It is this ChallengeS parkHTTPRequestHook code.
And it's pretty simple given a request and response.
It checks on stuff and goes through checks if it's null - if no, it doesn't process.
It checks if it's a particular verb with a particular URL.
If it is, then it does some stuff and I can go through and see what's going on and
there's some ability to check whether someone is logged in or not.
It's relatively simple code,
straightforward set of ifs, nothing really complicated in there.
So I'm going to run this test.
I'm going to right click and I'm going to run this test with coverage.
And what that's going to do in IntelliJ is
run the particular coverage tool that I've chosen, which I think is COCOMO.
And it's going to run the test with the
instrumented code and show me the coverage that was achieved.
So I'm going to say - Replace active
suites - here so I can see just the run from that.
And we can see a high level,
pretty low coverage there.
But I'm not really interested in the
package coverage because this test is designed to cover a particular class.
So I'm really only interested in this particular class here.
And we can see that I00% class coverage
because it's a test class, 100 percent method coverage.
Great.
But there's only three methods, so that's not that good.
78% line coverage, but that's not really good enough.
And when we look at the code, we'll see that.
Now some people might stop at this point
thinking that they've got 78% and they're good.
When we review it, we'll see that this code needs more than that.
Different types of code, the way it's written, the way it's laid out, the way
it's architected requires more or less coverage.
We make decisions about it.
The number itself is not the guidance.
So what I want, first of all, do in order to support my code review.
And I just want to zoom in on this.
I only want to see this particular class in the metrics.
So the way I'm going to do that is by editing the execution profile.
And in here in the run configuration, I'm going to go into code coverage.
And rather than saying I want you to look
at everything in this package, I'm going to say - I want you to look at a
specific class, and that is the ChallengerSparkHTTPRequestHook.
If I do that and say, OK, then run the test again with coverage.
Hopefully, I will only see that particular
class in the output because I'm trying to zoom in on
the specific area, so sometimes I'll have a test that is covering multiple classes.
I want to only include those particular classes in here.
So we can see that the only thing that's
reported now in scope is that particular class.
I'm zooming in. I'm having the execution profile to help
me review a particular set of tests and a particular set of classes.
So I've zoomed in there.
So let's see what we've got.
So I know in the report there that it's saying 78% line coverage.
Some people have standards.
It must be if it's 75% , we don't review it.
Some people go up to 90%, some people 100.
I think it depends on the class.
So here in this particular class, we can see, by the way it's written, that we're
achieving a lot of coverage no matter what we do.
Right, because we're always going to hit this method.
We're always going to go into this method.
We're always going to hit this line.
We're always going to have to check this condition.
We may not always return.
So I know that I have something in here
which passes this null and executes the return.
So I probably have a test purely for that path, which is good.
I can see that this is fully covered.
So I have a test that says get these
/challenges and then presumably it checks that the challenge is passed.
Here, I can see that I've got something
that's hit, "GET" and "heartbeat" but hasn't passed.
Now, this is interesting
because what I've got here is the reason that both these lines are passed is not
because I have a test for GET /heartbeat, I have a test for GET /challenges.
And this condition GET is exercised for my test for GET /challenges, because
the way the code is written, it's a series of "ifs".
So I do this "if" then the next "if" then
the next "if" - it goes through the whole code linearly.
So because I'm passing in GET /challenges,
we pass this condition because the method equals GET.
So this condition is passed because it's an && statement, I have to
also check whether the path equals /heartbeat.
In this case, the path doesn't equal
/heartbeat because it's /challenges, but we've still achieved line coverage there.
I haven't done the important thing, which is exercise the code under those
conditions and make it pass, which is what the test will really be checking for.
We can see here on DELETE, I've only
covered half of this condition where I DELETE, but I don't pass in /heartbeat so
I can see that I don't pass in /heartbeat because this isn't covered.
Now, one other thing that's interesting
here is the code formatting is helping me review this.
I can see here that I've got DELETE and I don't have /heartbeat.
And I can see that because green, we covered the delete pink.
We haven't covered this line.
We haven't covered the /heartbeat.
What happens if I change the formatting here and rerun the test?
So this is the DELETE_ HEARTBEAT.
So I'm going to go back to my test and run
the test classes with coverage, run test with coverage and we'll see what happens.
Remember, all I've done is change the
formatting, so I'm not expecting the numbers to change.
And I'm not really expecting any different results at all.
So I now have 80% line coverage, before it was 78%.
So by combining those lines, I've managed to increase the coverage.
Go us! We didn't change anything of importance.
But we're getting more coverage.
So this is why some of the metrics are a little bit hard to take as just numbers.
And also, when I review my code here, I
can see that this line DELETE /heartbeat is showing as covered.
Now, I know it isn't covered,
but I know why it's showing me this.
It's showing me this because we have to
exercise this "if" because all the 'ifs" are being exercised.
So "if" method equals delete, it isn't we won't go any further.
But this is on one line.
We've achieved line coverage for this - Go us!
So formatting makes a difference.
Formatting makes a difference in terms of the reported coverage, but also makes a
difference in terms of our ability to review that coverage easily.
So when I review the code, I can see that
I don't really need to care very much about the conditions.
Sometimes if we've got an "if" statement
and these are all "or"conditions, then I have to really care about the tests
that are coming in because I've got line coverage from that whole condition
statement, but it's an "or" so I need to say.
Does this happen or does this happen?
I need to have tests for at least both of those coming through, potentially neither.
So those three of them, I need more coverage.
When it's an && I need to make sure that it doesn't go in and it does go in.
And most of the time it doesn't go in is going to be covered by something else.
So I need to make sure that I have
something that covers that entire condition.
And it's a little bit easier.
Condition coverage or the boolean predicates within that condition have to
be analysed because the coverage is not going to report on that level.
The formatting might show me some
indicators that certain conditions are there.
But when I see complicated things here, I have to review it not just from code
coverage, but what the actual test itself is doing.
So in terms of line conditions, there are some lines here that are really important.
This challenger's pass.
This is the whole point of this method to
pass these particular challenges when certain conditions are met.
And I can see that I don't have any coverage for this.
I don't have any coverage for this.
So I know that there are gaps in the test code that is driving this.
And regardless of whether I've got 76%,
78%, 80% coverage, I do not have enough coverage.
And looking through this method, I suspect that once I've got tests for this,
I will actually need 100% coverage on this class to
have it tested to a level where I can trust refactoring over time, because it's
not really about the numbers, it's about how the code is written.
And if this code was written differently,
the line coverage would give me a better indication.
If each one of these conditions I knew was completely separate.
And after doing this, it just did a return and exited out the method.
My line coverage with this point would be
really low because I wouldn't even come through to hitting these methods.
But because of the way the code is currently written, I'm getting a lot of
code coverage from just exercising code rather than testing code.
Now, what I find interesting is when I'm talking to people about coverage, when I
talk about coverage, they very often hear code coverage.
What I'm really talking about is test coverage.
In fact, what I'm really talking about is model coverage.
Code coverage is using the code as a model and showing us how much of that model we
have covered when we have exercise that code externally by something else.
Very often what I'm talking about
coverage, I'm talking about a logical concept.
I have a set of conditions in mind that I want to know whether they're coverage.
Code coverage is just a side effect, an easy way of measuring that.
And some of those conditions may well be boolean parts of an overall condition.
They may be in an "or" statement.
Code coverage isn't going to tell me that.
I have to know the model that I want to
know covered and have to review the code coverage and the actual code that's been
implemented and the test coverage and evaluate it separately.
So when I'm talking about coverage, it's a bigger concept than just code coverage.
If I haven't covered all the code with my
test, then I know that either the code isn't related to the model that I'm trying
to test because when I write tests, it's not test to cover this code.
It's tests to cover a model set of
requirements, a set of conditions a set of states.
It's a set of stuff that I want to know whether it happens or not,
the fact that it has implemented my code is almost a side effect.
And if I haven't covered all my code and
my tests, then I know I've got a gap in my test coverage when I expect my test
to be the full set of information about what that code is supposed to do.
So I know I have gaps, so I'm going to go away, write more tests to cover this.
And I suspect when I do, I will get 100%
coverage because that's what's important for this particular class.
Now, in this example, I expect my test
code to completely be the only thing that covers this.
So I can run that particular test.
I can focus in on that particular class.
I can get a good indication of - have I
successfully implemented the model that I'm trying to do?
The set of requirements that this test embodies?
Have they been covered in this code?
I can use code coverage in a fairly good way to do that.
I can see that there's a danger because I
can achieve a lot of code coverage by exercising the code, not testing it.
I could rewrite it to deal with that.
But that's not my focus at this point in
time, because it's obvious to me that I have gaps.
The conditions here are simple enough that
I don't really have to worry about a lot of boolean combinations.
So that's good.
But I can see that from the review in the code, not from the coverage metrics.
By formatting the code differently, I get
a better indicator that I've not covered it.
But really line coverage will tell me whether I met this condition or not.
I don't really need to format it to see whether I've hit this condition or not,
because when this condition passes, then I will have exercised this line.
So I don't really have to worry too much
about lots of different combinations and booleans I'm really targeting at this
implementation set of lines and line coverage.
When it hits a high enough amount will tell me that.
So in this instance, I'm probably aiming for 100% coverage.
Other classes might require more tests.
Even when we've got to 100% coverage.
I might require more tests because I have to cover more combinations.
This is why numbers of coverage isn't good enough.
We have to do the review in combination
with the numbers and in combination with the actual test reviewing it and in
combination with the model that these tests implement, because it's really the
model that these tests implement that we're trying to cover.
And the code coverage is a side effect, we have measuring that.
Now, when I come back to write my code, I'm using this as a guide because I know
there's a gap, but I'm not writing code to cover this line.
I'm writing code to cover the gaps in the
model that are implemented in the test, in the code.
It's the model that's driving this that I'm trying to cover, not the code, right?
Tests are a way of implementing that coverage.
Code coverage is a way of monitoring that coverage.
But the model that sits on top of this is really the thing that I'm trying to cover.
So when I talk about coverage, I'm really talking about that model coverage.
But I can use code coverage as a tool to help me in my review process.