Isolation frameworks are intrusive by nature. Yes, Isolator included. That means, they have “knowledge” of the code inside the component under test. Big surprise there, right?
One the major detriments for people who have already ventured into the land of unit-testing is cost of test maintenance. It could be that the tests are not unit-testy enough (more like integration tests). Or, the test contains too many isolation statements. By using intrusive elements in the test code, you’re introducing this risk
Is writing tests for this code worth it? I’ve looked at procedural VB code (only 3 years old), with no tests, where functions were hundreds of lines long. Tests for these functions will not be short either.
The developers of this VB code cannot refactor it without the tests (they actually need to get permission to refactor, as this is a medical application, and they are not likely to get it without making sure there’s no risk involved in changing the code). I think tests are worth writing to have the flexibility to change the code, although it may incur the price of maintaining the test code later when refactoring. Overall, I think the cost would be lower.
So let’s say I have code to test. It has layers – Data access, business logic, dependency injection, CSLA, whatever. I usually want to test logic, so I’ll concentrate on a business logic component. With Isolator, I can choose where to draw the line – where to isolate the dependencies. With this choice I can make a brittle test, that will break when the inside code changes, or more robust one.
Here’s a simple example. I have a data class, called Orders:
1 |
<span class="kwrd">public</span> <span class="kwrd">class</span> Orders<br />{<br /> <span class="kwrd">private</span> List<Order> orderList = <span class="kwrd">new</span> List<Order>();<br /><br /> <span class="kwrd">public</span> List<Order> OrderList<br /> {<br /> get { <span class="kwrd">return</span> orderList; }<br /> }<br /><br /> <span class="kwrd">public</span> <span class="kwrd">void</span> ReadOrderData(<span class="kwrd">string</span> connectionString)<br /> {<br /><br /> <span class="kwrd">string</span> queryString =<br /> <span class="str">"SELECT OrderID, CustomerID FROM dbo.Orders;"</span>;<br /><br /> <span class="kwrd">using</span> (SqlConnection connection =<br /> <span class="kwrd">new</span> SqlConnection(connectionString))<br /> {<br /> SqlCommand command =<br /> <span class="kwrd">new</span> SqlCommand(queryString, connection);<br /> connection.Open();<br /><br /> SqlDataReader reader = command.ExecuteReader();<br /><br /> <span class="kwrd">while</span> (reader.Read())<br /> {<br /> orderList.Add(<br /> <span class="kwrd">new</span> Order((<span class="kwrd">int</span>) reader[0], (<span class="kwrd">int</span>) reader[1]));<br /> }<br /><br /> reader.Close();<br /> }<br /> }<br />} |
As you can see, the ReadOrderData method goes to the database, fills the orderList with Order objects it creates from the database. So in my test for a business logic component that uses Orders, I can write the following lines to fake all database access:
1 |
[TestMethod]<br /><span class="kwrd">public</span> <span class="kwrd">void</span> FakeDataObjects()<br />{<br /> var fakeConnection = Isolate.Fake.Instance<SqlConnection>();<br /> Isolate.Swap.NextInstance<SqlConnection>().With(fakeConnection);<br /><br /> var fakeCommand = Isolate.Fake.Instance<SqlCommand>();<br /> Isolate.Swap.NextInstance<SqlCommand>().With(fakeCommand);<br /><br /> var fakeReader = fakeCommand.ExecuteReader();<br /> <br /> Isolate.WhenCalled(()=>fakeReader.Read()).WillReturn(<span class="kwrd">true</span>);<br /><br /> Isolate.WhenCalled(()=>fakeReader[0]).WillReturn(1);<br /> Isolate.WhenCalled(()=>fakeReader[1]).WillReturn(2);<br /><br /> Isolate.WhenCalled(()=>fakeReader.Read()).WillReturn(<span class="kwrd">false</span>);<br /><br /> Orders orders = <span class="kwrd">new</span> Orders();<br /> orders.ReadOrderData(<span class="str">"MyDB"</span>);<br /><br /> Assert.AreEqual(1, orders.OrderList.Count);<br /><br />} |
See that? Even with Isolator’s API (which reduces the necessity to specify isolation for every line) it still has 9 lines of isolation statement. Each line has indirect relation to a line code inside the Orders object. That’s at least 9 possibilities of test failure, if and when I change the data access code.
I can also draw the isolation line at a more higher level – skip the data access bit, and return a fake List:
1 |
[TestMethod]<br /><span class="kwrd">public</span> <span class="kwrd">void</span> FakeOrder()<br />{<br /> Orders orders = <span class="kwrd">new</span> Orders();<br /> <br /> List<Order> fakeList = <span class="kwrd">new</span> List<Order>();<br /> fakeList.Add(<span class="kwrd">new</span> Order(1,2));<br /> <br /> Isolate.WhenCalled(()=>orders.OrderList).WillReturn(fakeList);<br /> <br /> Assert.AreEqual(1, orders.OrderList.Count);<br /><br />} |
Now I only have a single line that relates to the code under test. We’ve reduced the risk of the test code breaking because of changes in the production code.
In general, I’d say the line of isolation should be as close to the tested code. You will use less isolation statements, that will make for a more robust test.