Refactoring code that uses new operator to be more testable
I have the following code and am trying to unit test it:
public override IRenderable GetRenderable()
{
var val = SomeCalculationUsingClassMemberVari开发者_JS百科ables();
return new EquationRenderable(val);
}
It seems like I want to use a factory here so I can separate the creation of the IRenderable from this class. The problem is that I have many of these classes that create different IRenderables that are constructed in different ways, so I would need to implement a new factory method for each one. What is the best way to solve this problem?
Good question.
First of all, feeling tempted to use AbstractFactories everywhere smells a bit as DI container is not used the "right" way or design could be improved.
But sometimes I've also come across this problem. I see following:
- using AbstractFactory/Factory and Inject it. For C# you have to advantage that you can pass delegates, acting as interface for the creation of instance.
- 'new' is OK, simply test the output of 'new'.
- Stub the call of 'new' inside extracted method (hacky!!)
Injection got mentioned already so I won't repeat. I am more into Java so please excuse some syntax errors.
Test the output of 'new'
I often use this, if the 'new' created instances are domain-objects and not services. Because it is returned directly in method I can test the direct output with my test.
Prod-Code:
...
public override IRenderable GetRenderable()
{
var val = SomeCalculationUsingClassMemberVariables();
return new EquationRenderable(val);
}
Test Case:
...
[Test]
public void test_new()
{
SUT sut = ...;
IRenderable r = sut.GetRenderable();
assertTrue(r instanceof EquationRenderable);
}
Stub the call of 'new' itself
Testing direct output from above is only possible if you somehow get it as return value. Things get more complicated if the "sideeffect" of your code are indirect outputs, which you can't sense directly by the return value. If so I often extract-method of the new-creation and then have it under control in my test. This is yucky and I more use it to go safe with my test and start more refactoring later (DI and factories). I sometimes do this in legacy code where services are created with 'new' directly and refactoring to DI is too risky without tests.
Prod-Code:
...
public override IRenderable GetRenderable()
{
var val = SomeCalculationUsingClassMemberVariables();
return createEquationRenderable();
}
public IRenderable createEquationRenderable()
{
return new EquationRenderable(val);
}
Test Case:
...
class Stubbed : SUT
{
boolean called = false;
public override EquationRenderable createEquationRenderable()
{
called=true;
return MyMock();
}
}
[Test]
public void test_new()
{
Stubbed sut = new Stubbed();
sut.GetRenderable();
assertTrue(sut.called);
// do further stuff on MyMock
}
I know, the example is overkill and a bit senseless, it is just for describing the idea. I am sure above could be shortcutted with mocking-frameworks for C#. Anyway testing the return-value direct output is more trivial and better approach here.
Maybe you have a more detailed example?
depending on the uniformity of your concrete IRenderable constructors you can use the following pattern for factory creating
public IRenderable CreateInstance<T>(object calculation) where T : IRenderable
{
Activator.CreateInstance<T>(new[] { calculation });
}
or if you have many different constructors you can use the params keyword to pass arbitrary amounts of arguments
public IRenderable CreateInstance<T>(params object[] args) where T : IRenderable
{
Activator.CreateInstance<T>(args);
}
To be able to do some kind of runtime check of the arguments you use this code before calling the Activator.CreateInstance
var types = args.Select(o => o.GetType()).ToArray();
var c = typeof(T).GetConstructor(types);
if (c == null)
{
throw new InvalidOperationException("No matched constructor")
}
A better way may be to simply unit test the code as is instead of refactoring it. Technically, this can be done by using a suitable mocking tool such as TypeMock Isolator or Microsoft Moles (there is a third one which I don't remember now).
精彩评论