The code to test
The class to write tests for is intentionally simple; it has a single method that takes a string which is used to look up a value in a file, and modifies that string before returning it. Because of the dependency on a file, the code doesn't run from a unit test without that file being present.
public class Builder
{
public string BuildString(string configName)
{
var retVal = ConfigReader.GetConfig(configName);
return "xx" + retVal + "xx";
}
}
The ConfigReader class also has a single method, that reads a value from a custom configuration file:
{
public string BuildString(string configName)
{
var retVal = ConfigReader.GetConfig(configName);
return "xx" + retVal + "xx";
}
}
public class ConfigReader
{
public static string GetConfig(string setting)
{
using (var reader = File.OpenText("..\app.config"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var elements = line.Split('=');
if (elements[0] == setting)
{
return elements[1];
}
}
}
return null;
}
}
{
public static string GetConfig(string setting)
{
using (var reader = File.OpenText("..\app.config"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var elements = line.Split('=');
if (elements[0] == setting)
{
return elements[1];
}
}
}
return null;
}
}
Add virtual method
This is the technique that I highlighted in my original post. It involves creating a virtual method in the class under test, which in a production setting will delegate the call to the existing static method. In a test scenario though, it allows the class under test to be subclassed, and override the virtual method to return a dummy result.Notes
- This technique can only be used when the class under test is not sealed
- It requires no changes to the static class
Steps to implement:
Class under test- Add a virtual method
- Delegate the call to the static method to the new virtual method
- Update calls to the static method to call the new method
public class Builder
{
public string BuildString(string configName)
{
// 3
var retVal = GetConfig(configName);
return "xx" + retVal + "xx";
}
// 1
protected virtual string GetConfig(string configName)
{
// 2
return ConfigReader.GetConfig(configName);
}
}
Test class
{
public string BuildString(string configName)
{
// 3
var retVal = GetConfig(configName);
return "xx" + retVal + "xx";
}
// 1
protected virtual string GetConfig(string configName)
{
// 2
return ConfigReader.GetConfig(configName);
}
}
- Create a subclass of the class under test
- Override the virtual method to return a known result
- Create an instance of the subclass as the test target
[TestFixture]
public class BuilderTests
{
[Test]
public void BuildString()
{
// 3
var target = new FakeBuilder();
Assert.That(target.BuildString(""), Is.EqualTo("xxExpectedxx"));
}
// 1
private class FakeBuilder : Builder
{
// 2
protected override string GetConfig(string configName)
{
return "Expected";
}
}
}
public class BuilderTests
{
[Test]
public void BuildString()
{
// 3
var target = new FakeBuilder();
Assert.That(target.BuildString(""), Is.EqualTo("xxExpectedxx"));
}
// 1
private class FakeBuilder : Builder
{
// 2
protected override string GetConfig(string configName)
{
return "Expected";
}
}
}
Delegate to instance method
This technique involves adding an instance method to the dependency class, and extracting its interface. A stub of this interface can then be passed to the class for testing.Notes
- This technique can only be used when the dependency class is not marked static
Steps to implement:
Dependency class- Add an instance method
- Delegate from the instance method to static method
- Extract the interface
- Implement the interface
// 3
public interface IConfigReader
{
string GetConfiguration(string setting);
}
//4
public class ConfigReader : IConfigReader
{
// 1
public string GetConfiguration(string setting)
{
// 2
return GetConfig(setting);
}
public static string GetConfig(string setting)
{
using (var reader = File.OpenText("..\app.config"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var elements = line.Split('=');
if (elements[0] == setting)
{
return elements[1];
}
}
}
return null;
}
}
Class under test
public interface IConfigReader
{
string GetConfiguration(string setting);
}
//4
public class ConfigReader : IConfigReader
{
// 1
public string GetConfiguration(string setting)
{
// 2
return GetConfig(setting);
}
public static string GetConfig(string setting)
{
using (var reader = File.OpenText("..\app.config"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var elements = line.Split('=');
if (elements[0] == setting)
{
return elements[1];
}
}
}
return null;
}
}
- Add a new constructor to accept an implementation of the interface
- Update existing constructor(s) to create default implementation
- Update calls to static method to call interface method
public sealed class Builder
{
private IConfigReader configReader;
// 2
public Builder()
: this(new ConfigReader())
{ }
// 1
internal Builder(IConfigReader ConfigReader)
{
this.configReader = ConfigReader;
}
public string BuildString(string configName)
{
// 3
var retVal = configReader.GetConfiguration(configName);
return "xx" + retVal + "xx";
}
}
Test class
{
private IConfigReader configReader;
// 2
public Builder()
: this(new ConfigReader())
{ }
// 1
internal Builder(IConfigReader ConfigReader)
{
this.configReader = ConfigReader;
}
public string BuildString(string configName)
{
// 3
var retVal = configReader.GetConfiguration(configName);
return "xx" + retVal + "xx";
}
}
- Stub the new interface
- Pass the stub to the new constructor
[TestFixture]
public class BuilderTests
{
[Test]
public void BuildString()
{
// 1
var configFileReader = MockRepository.GenerateMock<IConfigReader>();
configFileReader.Stub(fr => fr.GetConfiguration(Arg<string>.Is.Anything))
.Return("Expected");
//2
var target = new Builder(configFileReader);
Assert.That(target.BuildString(""), Is.EqualTo("xxExpectedxx"));
}
}
public class BuilderTests
{
[Test]
public void BuildString()
{
// 1
var configFileReader = MockRepository.GenerateMock<IConfigReader>();
configFileReader.Stub(fr => fr.GetConfiguration(Arg<string>.Is.Anything))
.Return("Expected");
//2
var target = new Builder(configFileReader);
Assert.That(target.BuildString(""), Is.EqualTo("xxExpectedxx"));
}
}
Wrap static class
This technique is to create an instance class that delegates calls to the static class, but allows an interface to be specified for the calling class to use.Notes
- This technique does not require any change to the dependency class
Steps to implement:
Wrapper class- Create a new class with an instance method
- Delegate from the instance method to the static methods
- Extract the interface of the new class
// 3
public interface IConfigReaderWrapper
{
string GetConfig(string setting);
}
// 1
public class ConfigReaderWrapper : IConfigReaderWrapper
{
// 2
public string GetConfig(string setting)
{
return ConfigReader.GetConfig(setting);
}
}
Class under test
public interface IConfigReaderWrapper
{
string GetConfig(string setting);
}
// 1
public class ConfigReaderWrapper : IConfigReaderWrapper
{
// 2
public string GetConfig(string setting)
{
return ConfigReader.GetConfig(setting);
}
}
- Add a new constructor to accept an implementation of the new interface
- Update existing constructor(s) to create the default implementation
- Change all static method calls to call instance methods on the new interface
public class Builder
{
private IConfigReaderWrapper configReader;
// 2
public Builder()
: this(new ConfigReaderWrapper())
{ }
// 1
internal Builder(IConfigReaderWrapper ConfigReader)
{
this.configReader = ConfigReader;
}
public string BuildString(string configName)
{
// 3
var retVal = configReader.GetConfig(configName);
return "xx" + retVal + "xx";
}
}
Test class
{
private IConfigReaderWrapper configReader;
// 2
public Builder()
: this(new ConfigReaderWrapper())
{ }
// 1
internal Builder(IConfigReaderWrapper ConfigReader)
{
this.configReader = ConfigReader;
}
public string BuildString(string configName)
{
// 3
var retVal = configReader.GetConfig(configName);
return "xx" + retVal + "xx";
}
}
- Stub the new interface
- Pass to stub to new constructor
[TestFixture]
public class BuilderTests
{
[Test]
public void BuildString()
{
// 1
var configFileReader = MockRepository.GenerateMock<IConfigReaderWrapper>();
configFileReader.Stub(fr => fr.GetConfig(Arg<string>.Is.Anything))
.Return("Expected");
// 2
var target = new Builder(configFileReader);
Assert.That(target.BuildString(""), Is.EqualTo("xxExpectedxx"));
}
}
public class BuilderTests
{
[Test]
public void BuildString()
{
// 1
var configFileReader = MockRepository.GenerateMock<IConfigReaderWrapper>();
configFileReader.Stub(fr => fr.GetConfig(Arg<string>.Is.Anything))
.Return("Expected");
// 2
var target = new Builder(configFileReader);
Assert.That(target.BuildString(""), Is.EqualTo("xxExpectedxx"));
}
}
Pass lambda as implementation of static method
This technnique is to use a property on the static class to hold the implementation of the static method, and pass a fake implementation for testing.Notes
- Does not need changes to the class under test
Steps to implement:
Static class- Extract the implementation of the static method to a private static method
- Add a static property to hold a lambda whose signature matches that of the private method
- Add a static constructor
- Default the lambda to call the private method
- Modify the static method to execute the lambda held in the property
public static class ConfigReader
{
// 2
internal static Func<string, string> getConfigImpl { get; set; }
// 3
static ConfigReader()
{
// 4
getConfigImpl = s => _GetConfig(s);
}
public static string GetConfig(string setting)
{
// 5
return getConfigImpl(setting);
}
// 1
private static string _GetConfig(string setting)
{
using (var reader = File.OpenText("..\app.config"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var elements = line.Split('=');
if (elements[0] == setting)
{
return elements[1];
}
}
}
return null;
}
}
Test class
{
// 2
internal static Func<string, string> getConfigImpl { get; set; }
// 3
static ConfigReader()
{
// 4
getConfigImpl = s => _GetConfig(s);
}
public static string GetConfig(string setting)
{
// 5
return getConfigImpl(setting);
}
// 1
private static string _GetConfig(string setting)
{
using (var reader = File.OpenText("..\app.config"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var elements = line.Split('=');
if (elements[0] == setting)
{
return elements[1];
}
}
}
return null;
}
}
- Set the new property on the static class to the fake implementation
[TestFixture]
public class BuilderTests
{
[Test]
public void BuildString()
{
// 1
ConfigReader.getConfigImpl = s => "Expected";
var target = new Builder();
Assert.That(target.BuildString(""), Is.EqualTo("xxExpectedxx"));
}
}
public class BuilderTests
{
[Test]
public void BuildString()
{
// 1
ConfigReader.getConfigImpl = s => "Expected";
var target = new Builder();
Assert.That(target.BuildString(""), Is.EqualTo("xxExpectedxx"));
}
}
Any more?
One other technique is to use an advanced mocking framework that can stub static methods; unfortunately none of the free ones I know have this feature.Can you think of any other techniques that I haven't covered? If so, please leave a comment and let me know.
The code for these examples is on GitHub, click here.
No comments:
Post a Comment