How do you test a set of methods that call each other without repeating yourself unnecessarily?
You are more than tired of debugging “List index out of bounds”, “Duplicates not allowed” and similar exceptions. You want some help from your code so you don’t have to hit “Next” until you are blue in the face to figure out the information not contained in the standard exception messages.
So you created a class that checks the input parameters before actually performing any manipulations on a list. Staying true to the DRY principle, you created a method that does the checking and that is called by all manipulation methods.
class AugmentedList { private List<string> _List = new List<string>(); public int Count { get { return _List.Count; } } public void ValidateInput(string key, int index) { // Input validation code } public void AddKey(string key) { AddKey(key, _List.Count); } public void AddKey(string key, int index) { ValidateInput(key, index); // ... snip ... } public void MoveKey(string key, int fromIndex, int toIndex) { ValidateInput(key, fromIndex); ValidateInput(key, toIndex); // ... snip ... } public void RemoveKey(string key, int index) { ValidateInput(key, index); // ... snip ... } }
Now you face a conundrum.
Should you repeat the tests for each caller?
When you wrote the ValidateInput
method you also wrote a fair number of tests to ensure it does what it should and won’t be tripped up inadvertently by future changes to the code.
Should you now repeat all those tests for each method that calls the ValidateInput
method?
After all, the tests for the manipulation methods aren’t complete if you don’t test that they check their input arguments? But repeating the ValidateInput
methods tests for a manipulation method sounds like a gross violation of duplication/copy-paste principles. Especially when you would do that for every manipulation method!
Repeating the tests means that they need to be kept in sync. And you really do not want to do that because it poses a maintenance burden you can do without.
Relax. There is no need to violate any principles.
How not to repeat tests
The testing strategy for such a validation method / action method combination is straightforward. All you have to do is:
- Test the heck out of the validation method.
- Do not duplicate these tests for the action methods.
- Do write a single test for each action method to show that it actually calls the validation method.
- Do write tests for each action method to check its behavior given invalid input.
- Test the heck out of each action method to check its behavior given valid input.
Code example
Start by creating a test class with a factory method to instantiate your class under test: the AugmentedList
class.
[TestClass] public class AugmentedList_Tests { private AugmentedList MakeAugmentedList() { return new AugmentedList(); } }
Test the heck out of the validation method
Add the tests you need to ensure that ValidateInput does what it needs to do and keeps doing it regardless of future changes to your code.
[TestMethod] [ExpectedException(typeof(AugmentedListEmptyStringError))] public void ValidateInput_EmptyString_ShouldThrow_AugmentedListEmptyStringError() { var list = MakeAugmentedList(); list.ValidateInput("", 0); } [TestMethod] [ExpectedException(typeof(AugmentedListDuplicateStringError))] public void ValidateInput_DuplicateString_ShouldThrow_AugmentedListDuplicateStringError() { var list = MakeAugmentedList(); list.AddKey("Duplicate"); list.ValidateInput("Duplicate", 0); } [TestMethod] [ExpectedException(typeof(AugmentedListNegativeIndexError))] public void ValidateInput_IndexNegative_ShouldThrow_AugmentedListNegativeIndexError() { var list = MakeAugmentedList(); list.ValidateInput("SomeKey", -1); } [TestMethod] public void ValidateInput_IndexEqualToCount_ShouldNotThrow_AnyError() { var list = MakeAugmentedList(); list.ValidateInput("SomeKey", 0); // Assert.DoesNotThrow } [TestMethod] [ExpectedException(typeof(AugmentedListIndexTooHighError))] public void ValidateInput_IndexHigherThanCount_ShouldThrow_AugmentedListIndexTooHighError() { var list = MakeAugmentedList(); list.ValidateInput("SomeKey", 1); }
With this bunch of tests you ensure that ValidateInput throws specific exception types instead of just plain Exception
s. You could add additional tests to check that the exceptions thrown actually contain the specific information the AugmentedList
class should have passed to the exception constructor. Thus ensuring that you actually do get the extra information you wanted.
The ValidateInput_IndexEqualToCount_ShouldNotThrow_AnyError
of course does not have an ExpectedException attribute. It does not have an Assert either. The test is intended to assure that no error is thrown for the given input arguments. Given that ValidateInput
does not have a return type, there isn’t much to assert on. Other test unit frameworks have an Assert.DoesNotThrow
method to cover this scenario. Unfortunately MSTest (the Microsoft Unit Testing Framework) does not have such a method. Adding a comment to the same effect is often a very useful way to make your intentions explicit.
Write a single test for each action method to show that it actually calls the validation method
Given that ValidateInput
throws exceptions on invalid input, it is pretty easy to ensure that each action method calls the validation method. You simply add a test for the action method, feed it invalid input and expect the exception.
[TestMethod] [ExpectedException(typeof(AugmentedListNegativeIndexError))] public void AddKey_ShouldCall_ValidateInput() { var list = MakeAugmentedList(); list.AddKey("Irrelevant", -1); }
If AddKey
does not call (or no longer calls) ValidateInput
, bets are on that this test starts failing because no exception is thrown.
Of course some clever prankster could thwart this check by checking for this input in X and returning the same exception. But you have to draw the line somewhere. You are not protecting against hackers but against someone forgetting to use ValidateInput
and/or inadvertent removal of the call to ValidateInput
.
Write tests for each action method to check its behavior given invalid input
All your action methods should still behave correctly when faced with invalid input. They should at the very least catch the exceptions thrown by ValidateInput
and proceed accordingly.
For example when AddKey
is given invalid input, it should not only use ValidateInput
to discover it, but it should also not add any invalid input to your list. So you would want a test to check that given invalid input, AddKey
leaves the contents of the list as they were.
Similarly, as the AugmentedList
does not allow duplicates, you should have a test that shows that AddKey
does not change the contents of the list when it is passed a duplicate value.
[TestMethod] public void AddKey_InvalidInput_ShouldNotChange_ContentsOfList() { var list = MakeAugmentedList(); var listCopy = MakeAugmentedList(); try { list.AddKey("", 0); } catch { // Ignore for the purpose of this test } Assert.AreEqual(string.Concat(listCopy), string.Concat(list)); } [TestMethod] public void AddKey_StringAlreadyPresent_ShouldNotChange_NumberOfItems() { var list = MakeAugmentedList(); var listCopy = MakeAugmentedList(); list.AddKey("Duplicate"); listCopy.AddKey("Duplicate"); try { list.AddKey("Duplicate", 0); } catch { // Ignore for the purpose of this test } Assert.AreEqual(listCopy.Count, list.Count); }
If you get the impression that you are repeating the input values that you fed ValidateInput
to test AddKey
, then you are … very much right.
Depending on the implementation of the validation method, the test to show that an action method actually calls the validation method can indeed have much the same implementation as the test to check an action method’s behavior on invalid input.
You still code them BOTH!
Even though it looks like un-dry, soggy code, it isn’t and in this case you do want to “repeat” yourself. [1] The intention of the tests is very different, which – if you do it right – is reflected in the names of the tests. The fact that these tests may have (almost) the same implementation is less important if not totally irrelevant. What is important is that each test documents and assures a different aspect of your class’ behavior and thus represents a different piece of knowledge about your class.
Test the heck out of each action method to check its behavior given valid input
With the tests in place that assure that ValidateInput
is called and that assure the correct behavior of your action methods when you pass them invalid input, you can now focus on testing the behavior of your action methods for valid inputs.
[TestMethod] public void AddKey_StringNotYetPresent_ShouldIncrease_NumberOfItems() { } [TestMethod] public void AddKey_StringNotYetPresent_ShouldReturn_StringAtGivenIndex() { } [TestMethod] public void AddKey_NoIndexParam_StringNotYetPresent_ShouldIncrease_NumberOfItems() { } [TestMethod] public void AddKey_NoIndexParam_StringNotYetPresent_ShouldReturn_StringAtEndOfList() { }
That’s it. Enjoy!
What code had you wondering how to test it? I’d love hearing from you by email or in the comments below. I read everything and will try to help where and as best I can.
Notes
[1] The idea behind DRY is far grander than avoiding code duplication. It is more about avoiding knowledge duplication in your application’s sources and that is not just code, but includes database schemas, installation scripts etc. See Orthogonality and the DRY Principle for an interesting read.
Leave a Reply