Mirrored Unit Tests

I’m not sure if the term of Mirrored Unit Tests actually exists, but I’ll explain in this post what I mean by it. You get this situation when you have a class under test, which has symmetric public methods, which change the internal state of the class and

Mirrored Unit Tests

I’m not sure if the term of Mirrored Unit Tests actually exists, but I’ll explain in this post what I mean by it.

You get this situation when you have a class under test, which has symmetric public methods, which change the internal state of the class and have logic that depends on the fact that one method was called before or not. To test this for one method, you will have to have tests for scenarios in which the other method was called or not. Then you will have to rewrite the same tests for the other method, in a very similar way, like in a mirror. From where the term: Mirrored Unit Tests. No matter how much you will try to remove duplicate code by extracting it into helper methods, your test code will get messy.

The solution, is an abstraction (of course J ). You make an abstract test class, in which you implement the tests for one method. Then you implement the abstraction for each method you wanted to test and you override the protected members that abstract the method under test. With this test design you get rid of the duplication and it works nicely with most of the unit testing frameworks.

Below I will detail this more, by giving an example.

I have a class which is used in an implementation which builds a lambda expression, from some other representation of code (less important for this example). The class looks like this:

[sourcecode language=”css”]
class BinaryExpressionBinder
{
private Expression left;
private Expression right;

public Expression ResultedExpression { get; private set; }

public void NotifyForRightNode(Expression rightExpr)
{
bool constantExpr = TrySetAsConstantExpression(rightExpr, LeftWasSet);

if (!constantExpr)
{
right = rightExpr;
SetFieldExpression();
}
}

public void NotifyForLeftNode(Expression leftExpr)
{
bool constantExpr = TrySetAsConstantExpression(leftExpr, RightWasSet);

if (!constantExpr)
{
left = leftExpr;
SetFieldExpression();
}
}

private bool LeftWasSet
{
get { return left != null; }
}

private bool RightWasSet
{
get { return right != null; }
}

private void SetFieldExpression()
{
if (LeftWasSet && RightWasSet)
{
BinaryLogicalExpression be = new BinaryLogicalExpression
{
Left = left,
BooleanOperator = op,
Right = right
};
ResultedExpression = be;
}
}

private bool TrySetConstantExpression(Expression memberExpr, bool otherMemberWasSet)
{
// some logic which is not relevant for this sample.
// This logic may set the result with a constant expression
}
}
[/sourcecode]

The important thing to notice is the two public methods: NotifyForRightNode() and
NotifyForLeftNode()
. They are symmetric: both in signature and also in implementation. They try to set the result as a constant expression. If that fails it remembers the notification argument and if the notification for the other node was called, then a binary expression is set as result. It is important to remember that the binary expression can be built only when both members are known.

Now let’s see how to test this. I will need to write unit tests against the public interface and not to relay at all on the implementation details. In this case I would need to write tests for NotifyForRightNode() with at least the following scenarios: left node was not set yet, and a constant expression can or (other scenario) cannot be built; left node was already set and a constant expression can or (other scenario) cannot be built. So at least four scenarios for NotifyForRightNode(). Now the same tests have to be for the other method
NotifyForLeftNode()
, but in mirror (configuring that right node was set or not). As I said above, I will write the tests only for one method, in an abstract test class and then I will override just to specify the method under test.

[sourcecode language=”css”]
public abstract class BinaryExpressionBinderTests
{
protected abstract void NotifyForNodeUnderTest(Expression expr);
protected abstract void NotifyForOtherNode(Expression expr);

protected BinaryExpressionBinder Binder;

[TestMethod]
public void NotifyForNode_OtherNodeWasNotifiedAndConstantExpressionCannotBeBuild_ResultIsBinaryExpression()
{
//arange
Binder = ConfigureBuilder();
Expression dummyExpr = GetDummyExpression();
NotifyForOtherNode(dummyExpr);

//act
NotifyForNodeUnderTest(dummyExpr);

//assert
Expression actual = Binder.ResultedExpression;
Assert.IsInstanceOfType(actual, typeof(BinaryExpression));
}

// other tests …
}

[TestClass]
public class BinaryExpressionBinderTestsForLeftNode : BinaryExpressionBinderTests
{
protected override void NotifyForNodeUnderTest(Expression expr)
{
Binder.NotifyForLeftNode(expr);
}

protected override void NotifyForOtherNode(Expression expr)
{
Binder.NotifyForRightNode(expr);
}
}

[TestClass]
public class BinaryExpressionBinderTestsForRightNode : BinaryExpressionBinderTests
{
protected override void NotifyForNodeUnderTest(Expression expr)
{
Binder.NotifyForRightNode(expr);
}

protected override void NotifyForOtherNode(Expression expr)
{
Binder.NotifyForLeftNode(expr);
}
}
[/sourcecode]

So, less code, lower maintenance costs. It wouldn’t be right to test only one method, even though the VS (or other measuring tool) would say that I have a good coverage. The good coverage comes only because my implementation avoids duplicates and calls helper methods. What I want is to have a good coverage and this not to depend on the implementation. When I will refactor the production code, my tests have to stay green and valid without the need of being adjusted. Because that’s how good tests are!

Code Design
Blog

We share our insights and experiences from working on client projects and teaching developers.

Our articles blend technical expertise with real-world challenges, offering a unique perspective on Code Design.

For us, Code Design means structuring code to ensure predictability and making it easy to manage and adapt long after it's written.