Yesterday, I did some coding practice: using test-driven development to implement a library that makes a service call and then parses the data. There were a few things left undone, so today I coded those up (also using TDD).
You can get the code for this in the sunset-tdd branch of the GitHub project: jeremybytes/house-control. And a collection of all the articles for this project are available here: Rewriting a Legacy Application.
As a reminder, the classes that we are working with are SunsetTDD and SunsetTDDTest.
The first step is pretty easy: we just need to implement the "GetSunrise" method of our class. This is pretty similar to our "GetSunset" method.
We'll start with a test:
This looks like our test for "GetSunset" that we saw yesterday. It uses a mock object to get the service data, and our expected output is February 15, 2015 at 6:35:18 a.m.
Of course, this fails. That's because our method is still not implemented:
But we'll grab the functionality from our other method (with appropriate changes for sunrise):
With this code in place, we have a passing test. (And we can see this in the "1/1 passing" note of the Code Lens information.)
Now we need to move on to something a bit more difficult: caching. We don't want to make a service call every single time we need the sunrise or sunset data. So, we'll make the service call and save off that data (with the date that the data refers to). Since we're generally only dealing with on day at a time, this is sufficient to reduce the number of service calls that the application needs to make.
Caching Test #1
We'll start by writing a unit test to test for the caching functionality:
Just like with other tests, we create a mock object to supply us with the data. But unlike the other tests, we're calling the "GetSunset" method twice with the same date parameter. Ideally, we should only make one service call in this scenario.
And that's exactly what Moq allows us to check for. Notice the last line of our method. We're using our mock object ("serviceMock") and using the "Verify" method to see how many times a particular method is called. In this case, we want to know how many times the "GetServiceData" method is called. This is the method that actually gets data from the service.
We can see from the 2nd parameter of the "Verify" method that we are expecting this to be called only one time. But that's not what's happening (as we can see from the failing unit test).
If we look at the unit test message, we see why the test failed:
This tells us exactly what we expect to find. Our unit test is expecting that the service will be called once, but it is actually called 2 times. No surprise since we have not yet implemented the cache.
To implement the cache, We'll add 2 fields to our class:
These will hold the data that actually comes from the service as well as the date that was used as a parameter to get that data. Again, since we're only dealing with one date at a time (generally), this simple cache will work for us.
To populate the cache, we've created a new method:
This method is fairly straight-forward. It checks to see if the cache date is the same as the date that we're looking for. If it is *not* the same, then it calls the service to get fresh data and populates our 2 cache fields. If the dates do match, then we simply return our cached data.
The last step is to use this in our "GetSunset" method. This is as simple as swapping our our call to "SunsetService.GetServiceData" (that actually calls the service) with our new method "GetServiceData" (which will use the cache).
With this in place, our test passes. But we have a few more scenarios to test.
Caching Test #2
Our next scenario is to test the cache by calling "GetSunrise" multiple times. Here's our test:
And this test fails. That's because we still need to update the "GetSunrise" method to use our new caching method.
And that's easy enough to do:
Now our test passes. Now in my normal coding, I would have a tendency to have updated *both* the "GetSunset" and "GetSunrise" methods at the same time. But since I'm practicing my TDD, I've been resisting the urge to update code before I've written a test for it.
Just because our cache is working doesn't mean that we're done with testing.
Caching Test #3
As another scenario, I want to check that "GetSunset" and "GetSunrise" both share the same cache. Here's the test:
Notice that instead of calling the same method twice, we call "GetSunrise" one time and "GetSunset" one time (both with the same date parameter). Our expectation is that our service only gets called once due to the cache.
And that's exactly what we see. This test passes without us needing to modify the code.
Caching Test #4
As our last test, I want to make sure that the cache is *not* used when we call methods with different date parameters. Here's that test:
Here we have 2 different date variables ("date1" and "date2"). And we use these to call the "GetSunset" method with different parameters. This time, our "Verify" is a little different: it expects that our service will be called exactly 2 times.
And that's the behavior that we get. This test passes without needing to modify any of our code.
"Test First" Does Not Mean We Stop When the Code is Written
The moral of this is that we aren't necessarily done creating tests after our code is in place. We need to make sure that we test various scenarios to make sure that we have a good set of tests in place. So even if we have "TDD'd" all of our code creation, we need to check for thoroughness in our tests.
There are probably a few more test scenarios that I need for this library. And I'll review the tests some more to look for gaps. (This is one of the things I hope that Smart Unit Tests will be able to help us with once it is released.)
More coding practice is good. I'm glad that I kept going with this. I wasn't quite sure how I would handle the caching functionality. And it turns out that I ended up with similar code to my original implementation. The methods are a little bit different (and I think a little bit cleaner).
The more I do this, the more I understand the advantages and disadvantages of using TDD. I still need to try this with different types of code (including libraries that call into a database or do data validation). But this is a good start, and I'll keep exploring.
I encourage you to explore different techniques and technologies. Pick the ones that are most useful to you and incorporate them into your development process.