How to unit test Roslyn Rewriter?

Yesterday, I wrote a SyntaxRewriter that adds null test on method body.


So it could be very easy like this one:


public int Foo1(OrderDetail od)
{
    return od.Product.Category.CategoryID;
}


That becomes:


public int Foo1(OrderDetail od)
{
    if (od == null || od.Product == null || od.Product.Category == null)
        return default(int);
    return od.Product.Category.CategoryID;
}

Or it could be a little bit more complex like this one:


public int Foo2(OrderDetail od)
{
    if (od.Product.CategoryID == 0 && od.Order.OrderID == 0 && od.Order.Customer.CompanyName.Length == 0)
        return 1;
    return 2;
}

That becomes:


public int Foo2(OrderDetail od)
{
    if (od == null || od.Product == null)
        return default(int);
    bool waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Product.CategoryID == 0;
    if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
    {
        if (od.Order == null)
            return default(int);
        waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Order.OrderID == 0;
        if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
        {
            if (od.Order.Customer == null || od.Order.Customer.CompanyName == null)
                return default(int);
            waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Order.Customer.CompanyName.Length == 0;
        }
    }

    if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
        return 1;
    return 2;
}

Or it could be really complex.




In order to be sure about my code, I decided to use unit tests.


But how to test meta-code?


Because my rewriter could add some new variable using a Guid in the name, I could not basically compare strings.


So I first try to use Regex like this:


So, for example, with Foo2, my unit test was the following:


[TestMethod]
public void TestMethod2()
{
    var compilationUnitSyntax = GetCompilationUnitSyntax();
    Assert.IsTrue(Regex.IsMatch(compilationUnitSyntax.ChildNodes().OfType<NamespaceDeclarationSyntax>().First()
        .ChildNodes().OfType<
ClassDeclarationSyntax>().First().ChildNodes().OfType<MethodDeclarationSyntax>()
        .First(m => m.Identifier.ValueText ==
"Foo2").NormalizeWhitespace().ToString(),
@"^\s*public\s+int\s+Foo2\s*\(\s*OrderDetail\s+od\s*\)\s*
\s*\{\s*
\s*if\s*\(od\s*==\s*null\s*\|\|\s*od.Product\s*==\s*null\)\s*
\s*return\s+default\s*\(\s*int\s*\)\s*;\s*
\s*bool\s+(waqsLogical[\w\d]{32})\s*=\s*od.Product.CategoryID\s*==\s*0\s*;\s*
\s*if\s*\(\s*\1\s*\)\s*
\s*\{\s*
\s*if\s*\(\s*od.Order\s*==\s*null\s*\)\s*
\s*return\s+default\s*\(\s*int\s*\)\s*;\s*
\s*\1\s*=\s*od.Order.OrderID\s*==\s*0\s*;\s*
\s*if\s*\(\s*\1\s*\)\s*
\s*\{\s*
\s*if\s*\(\s*od.Order.Customer\s*==\s*null\s*\|\|\s*od.Order.Customer.CompanyName\s*==\s*null\s*\)\s*
\s*return\s+default\s*\(\s*int\s*\)\s*;\s*
\s*\1\s*=\s*od.Order.Customer.CompanyName.Length\s*==\s*0\s*;\s*
\s*\}\s*
\s*\}\s*
\s*if\s*\(\s*\1\s*\)\s*
\s*return\s+1\s*;\s*
\s*return\s+2\s*;\s*
\s*\}\s*$", RegexOptions.Multiline));
}

But it was a pain to write it and test readability is very bad.


So I need to find another way.



I decided to analyze my code using Roslyn to compare the Rewriter SyntaxTree with an expected one.


In order to do it very easily, we can use Syntax Parse methods to build the expected one.



Now the pain is to compare two SyntaxTree. Indeed there are so many different SyntaxNode that it would be very very long to write all the code myself.


The good point is the fact that this code is very repetitive:


public class SyntaxNodeEqualityVisitor
{
    private readonly Func<SyntaxNode, SyntaxNode, bool?> _syntaxNodeComparer;
    private readonly Func<string, string, bool> _identifierComparer;

    public SyntaxNodeEqualityVisitor(Func<SyntaxNode, SyntaxNode, bool?> syntaxNodeComparer = null,
     Func<string, string, bool> identifierComparer = null)
    {
     _syntaxNodeComparer = syntaxNodeComparer;
        _identifierComparer = identifierComparer;
    }




   
public virtual bool AreEquals(SyntaxNode node1, SyntaxNode node2)
    {
     if (node1 == null)
         return node2 == null;
        if (node2 == null)
            return false;
        if (_syntaxNodeComparer != null)
        {
            bool? value = _syntaxNodeComparer(node1, node2);
            if (value.HasValue)
                return value.Value;
        }

        if (node1.GetType() != node2.GetType())
             return false;

        var accessorDeclaration1 = node1 as AccessorDeclarationSyntax;
        var accessorDeclaration2 = node2 as AccessorDeclarationSyntax
;
        if (accessorDeclaration1 != null && accessorDeclaration2 != null
)
            return
AreEqualsAccessorDeclaration(accessorDeclaration1, accessorDeclaration2);   
                                           
        var parameterList1 = node1 as ParameterListSyntax
;
        var parameterList2 = node2 as ParameterListSyntax
;
        if (parameterList1 != null && parameterList2 != null
)
            return
AreEqualsParameterList(parameterList1, parameterList2);    
                                           
        var bracketedParameterList1 = node1 as BracketedParameterListSyntax;
        var bracketedParameterList2 = node2 as BracketedParameterListSyntax
;
        if (bracketedParameterList1 != null && bracketedParameterList2 != null
)
            return
AreEqualsBracketedParameterList(bracketedParameterList1, bracketedParameterList2);



       
//...
    }


public virtual bool AreEqualsAccessorDeclaration(AccessorDeclarationSyntax node1, AccessorDeclarationSyntax node2)
    {
        int
attributeListsCount = node1.AttributeLists.Count;
        if
(node2.AttributeLists.Count != attributeListsCount)
         return false
;
        for (int
i = 0 ; i < attributeListsCount ; i ++)
         if
(! AreEquals(node1.AttributeLists[i], node2.AttributeLists[i]))
return false
;
        int
modifiersCount = node1.Modifiers.Count;
        if
(node2.Modifiers.Count != modifiersCount)
            return false
;
        for (int
i = 0 ; i < modifiersCount ; i ++)
            if
(node1.Modifiers[i].Kind != node2.Modifiers[i].Kind)
                return false
;
        if
(node1.Keyword.Kind != node2.Keyword.Kind)
         return false
;
        if
(! AreEquals(node1.Body, node2.Body))
         return false
;
        if
(node1.SemicolonToken.Kind != node2.SemicolonToken.Kind)
            return false
;
        return true
;
}

public virtual bool AreEqualsParameterList(ParameterListSyntax node1, ParameterListSyntax
node2)
{
        if
(node1.OpenParenToken.Kind != node2.OpenParenToken.Kind)
            return false
;
        int
parametersCount = node1.Parameters.Count;
        if
(node2.Parameters.Count != parametersCount)
            return false
;
        for (int
i = 0 ; i < parametersCount ; i ++)
            if
(! AreEquals(node1.Parameters[i], node2.Parameters[i]))
                return false
;
        if
(node1.CloseParenToken.Kind != node2.CloseParenToken.Kind)
         return false
;
        return true
;
}


   
//...
}

So instead of writing it myself, I wrote a T4 template that determines things to test using Reflection on SyntaxVisitor class.


Then, I add a RoslynSyntaxTreeComparer class:


public class RoslynSyntaxTreeComparer
{
    public static bool Equals(SyntaxNode node1, SyntaxNode node2,
     Func<SyntaxNode, SyntaxNode, bool?> syntaxNodeComparer = null,
Func<string, string, bool> identifierComparer = null)
    {
        return new SyntaxNodeEqualityVisitor(syntaxNodeComparer, identifierComparer).AreEquals(node1, node2);
    }
}

I built these two files in an assembly and I can now use it like this:


[TestMethod]
public void TestMethod2()
{
    var compilationUnitSyntax = GetCompilationUnitSyntax();
    Assert.IsTrue(AreEquals(
@"public int Foo2(OrderDetail od)
{
if (od == null || od.Product == null)
return default(int);
bool waqsLogical1 = od.Product.CategoryID == 0;
if (waqsLogical1)
{
if (od.Order == null)
    return default(int);
waqsLogical1 = od.Order.OrderID == 0;
if (waqsLogical1)
{
    if (od.Order.Customer == null || od.Order.Customer.CompanyName == null)
        return default(int);
    waqsLogical1 = od.Order.Customer.CompanyName.Length == 0;
}
}
if (waqsLogical1)
return 1;
return 2;
}", compilationUnitSyntax.ChildNodes().OfType<NamespaceDeclarationSyntax>().First().ChildNodes()
.OfType<
ClassDeclarationSyntax>().First().ChildNodes().OfType<MethodDeclarationSyntax>()
.First(m => m.Identifier.ValueText ==
"Foo2")));
}
 
private bool AreEquals(string expectedCode, SyntaxNode currentCode)
{
    var identifierNames = new Dictionary<string, string>();
    var identifierNames2 = new Dictionary<string, string>();
return RoslynSyntaxTreeComparer.Equals(Syntax.ParseCompilationUnit(expectedCode).Members[0], currentCode,
identifierComparer: (n1, n2) =>
        {
            if (n1 == n2)
                return true;
            if (n1.StartsWith("waqs"))
            {
                string identifierName;
                if (identifierNames.TryGetValue(n1, out identifierName))
                    return n2 == identifierName;
                identifierNames.Add(n1, n2);
                identifierNames2.Add(n2, n1);
               
return true;
            }
            return n1 == n2;
        });
}

So, now, my unit test is easier to write and easier to read.


And, for you, I publish my dll in a NuGet package: https://www.nuget.org/packages/Roslyn.UnitTests.Helpers


Hope that helps

PS: note that instead of using string for my expected code, I can code the expected method and use Roslyn to get its syntax tree.

This entry was posted in 10550, 16402, 17894, 17895, 18099. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>