Reputation: 5467
I have to work with some legacy code that I cannot change. I have to write classes of that implement an interface called "ITask" that has a method called "RunTask" that accepts a concrete type called Schedule e.g;
public void RunTask(Schedule thisSchedule)
{
//I have to do stuff with thisSchedule, no I can't fix that name...
}
Although the legacy code does not use unit testing I would dearly like to use it for my work but the problem is "thisSchedule". I have made a fake version of Schedule that derives from it and am attempting to take control over how all the methods within it function (it is probably a fool's errand). So far I have been successful by exploiting the alarming number of virtual methods or by using reflection but I have hit my first show-stopper
I cannot connect to the real database and I have a method that is often called;
public void BeginTransaction()
{
MyTransaction = MyConnection.BeginTransaction(IsolationLevel.RepeatableRead);
}
internal SqlConnection MyConnection = new System.Data.SqlClient.SqlConnection();
This throws an exception because the connection is closed, Ideally I would like to be able to set a flag stating that the method was called and then just swallow the exception but I would be happy to simply ignore the exception.
Anything, no matter how nasty that will allow me to get past this method call would be an acceptable answer. It's that or defeat.
I am not allowed to use paid-for services like typeMock or shims in visual studio enterprise.
EDIT
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Data;
using System.Data.SqlClient;
namespace PredictionServicesTests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void BeginTransaction_WhenCalled_SetsTransactionStartedToTrue()
{
Schedule schedule = new FakeSchedule();
schedule.BeginTransaction(); //It would be enough to simply get past this line to the Assert
Assert.IsTrue(((FakeSchedule)schedule).TransactionStarted); //This would be nice, but isn't vital
}
class Schedule
{
private SqlTransaction MyTransaction;
internal SqlConnection MyConnection = new System.Data.SqlClient.SqlConnection();
public void BeginTransaction()
{
MyTransaction = MyConnection.BeginTransaction(IsolationLevel.RepeatableRead);
}
}
class FakeSchedule : Schedule
{
public bool TransactionStarted { get; set; }
}
}
}
Please remember I cannot change the Schedule class, if only things could be that simple!
The ITask interface;
interface ITask
{
void RunTask(Schedule thisSchedule);
}
Upvotes: 2
Views: 2931
Reputation: 5467
The closest I could get was to create an interface named IFake that I would apply to FakeSchedule;
public interface IFake
{
Action GetFakeAction(Action action);
}
public class FakeSchedule : Schedule, IFake
{
public bool TransactionStarted { get; set; }
public Action GetFakeAction(Action action)
{
return () => TransactionStarted = true;
}
}
I then created the following extension method;
public static void Testable<T>(this T obj, Action action)
{
if(obj is IFake)
{
action = ((IFake)obj).GetFakeAction(action);
}
action();
}
This allowed me to call the BeginTransaction method like this;
[TestMethod]
public void BeginTransaction_WhenCalled_SetsTransactionStartedToTrue()
{
Schedule schedule = new FakeSchedule();
var connection = new SqlConnection();
schedule.Testable(schedule.BeginTransaction);
Assert.IsTrue(((FakeSchedule)schedule).TransactionStarted);
}
Obviously this solution is far from ideal but it works, if I'm running a unit test then the call to begin transaction is diverted away from the real implementation.
The version I implement will need some work (my implementation of GetFakeAction is too dumb) but it's the closest I think I am going to get, the only other solution I can see would be to either use Prig or to follow NKosi's answer and connect to a dummy database.
Upvotes: 0
Reputation: 247018
The fact that you are "newing" up the SQL connection within the class makes it difficult to invert the control and make it more unit test friendly.
Another option would be to connect to a dummy database and use that connection. The dummy database would have just enough to allow the test to be exercised as an integration test.
That legacy code should be treated as 3rd party code you have no control over. And you should not waste time testing 3rd part code you have no control over. Common practice is to encapsulate 3rd party code behind an abstraction you do control and use that. The legacy code demonstrates technical debt that is being cashed in with the current difficulties that it is presenting because of poor design.
Not much else can be done given the current restrictions.
Upvotes: 1
Reputation: 32505
When it comes to unit testing, any external dependency needs to be isolated. Different frameworks have different ways of isolating external dependencies, as well as limitations of what they can isolate.
Many frameworks allow you to create a mock of an interface, so that you can provide an implementation for members in a controlled state. A lot of these frameworks also work well with abstract members of abstract classes. However, very few frameworks support the ability to provide an implementation on a concrete class (non-abstract member). The only two I'm aware which are available are:
Both of these frameworks utilize the .NET Profiler API to intercept member calls to replace them with code you provide. I'm not too familiar with TypeMock Isolator, but I am very familiar with Microsoft Fakes. The Microsoft Fakes framework supports generating an assembly (i.e. System.Fakes.dll) which contains classes that allow you to provide your own implementation on members. It supports creating Stubs against interfaces and abstract classes (equivalent to "mocks"), and Shims against concrete and static classes.
If you choose to use Microsoft Fakes, you'll first need to generate a fake assembly against System.Data.dll, which is where SqlConnection
resides. This will generate the ShimSqlConnection
class, and this class will contain members which allow you to provide alternative implementations of the SqlConnection
class' members. Here is an example of how that could be done:
[TestMethod]
public void SampleTest()
{
using (ShimsContext.Create())
{
// Arrange
SqlConnection.AllInstances.BeginTransactionIsolationLevel =
(instance, iso) => { ... };
// Act
// Assert
}
}
Of course, whether it is a mock, shim, stub, whatever... you should provide alternative implementation which simulates the behavior you want. Since you are replacing the SqlConnection.BeginTransaction(IsolationLevel)
member, you must still follow it's contract and expected behavior; you should not return a value or throw an exception that the actual implementation would not do. At my company, we utilize Microsoft Fakes to simulate scenarios such as:
Stream
is closed when we read from it?HttpWebRequest
?SqlDataReader.Read()
member)?In all of these scenarios, our goal was to isolate the implemented behavior of these members and replace them with implementations which could realistically occur in a controlled experiment.
Update:
I just noticed that the original poster stated "I am not allowed to use paid-for services like typeMock or shims in visual studio enterprise.". If that is a restriction, then you will have to find another tool that I am unaware of that utilizes the .NET Profiler API or utilize the framework yourself. Profiling (Unmanaged API Reference)
.
Upvotes: 1