File: Testing\CSharpTestMethodFinderTests.cs
Web Access
Project: src\src\Features\CSharpTest\Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.Features.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Features.Testing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests;
 
[UseExportProvider]
public class CSharpTestMethodFinderTests
{
    #region Xunit
 
    [Fact]
    public async Task TestFindsXUnitFactMethod()
    {
        var code = """
            using Xunit;
            public class TestClass
            {
                [Fact]
                public void Test$$Method1() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestMatchesXUnitFactInInnerClassMethod()
    {
        var code = """
            using Xunit;
            public class OuterClass
            {
                [Fact]
                public void TestMethod1() { }
            
                public class InnerClass
                {
                    [Fact]
                    public void Test$$Method2() { }
                }
            }
            """;
        await TestXunitMatchAsync(code, "OuterClass+InnerClass.TestMethod2");
    }
 
    [Fact]
    public async Task TestMatchesXUnitFactInOuterClassMethod()
    {
        var code = """
            using Xunit;
            public class OuterClass
            {
                [Fact]
                public void Test$$Method1() { }
            
                public class InnerClass
                {
                    [Fact]
                    public void TestMethod2() { }
                }
            }
            """;
        await TestXunitMatchAsync(code, "OuterClass.TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsXUnitFactAttributeMethod()
    {
        var code = """
            using Xunit;
            public class TestClass
            {
                [FactAttribute]
                public void Test$$Method1() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsXUnitTheoryMethod()
    {
        var code = """
            using Xunit;
            public class TestClass
            {
                [Theory]
                public void Test$$Method1() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsXUnitTheoryAttributeMethod()
    {
        var code = """
            using Xunit;
            public class TestClass
            {
                [TheoryAttribute]
                public void Test$$Method1() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsXUnitAliasedFactMethod()
    {
        var code = """
            using Xunit;
            using test = Xunit.FactAttribute;
            public class TestClass
            {
                [test]
                public void Test$$Method1() { }
            }
            """;
 
        // This asserts that we cannot find test methods with aliased attributes because we only do syntactic checks.
        await TestXunitAsync(code);
    }
 
    [Fact]
    public async Task TestFindsXUnitFactOnlySelectedMethod()
    {
        var code = """
            using Xunit;
            public class TestClass
            {
                [Fact]
                public void Test$$Method1() { }
 
                [Fact]
                public void TestMethod2() { }
 
                public void NotTestMethod() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsXunitMethodsInClassMethod()
    {
        var code = """
            using Xunit;
            public class Test$$Class
            {
                [Fact]
                public void TestMethod1() { }
 
                [Fact]
                public void TestMethod2() { }
 
                [Theory]
                public void TestMethod3() { }
 
                public void NotTestMethod() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1", "TestMethod2", "TestMethod3");
    }
 
    [Fact]
    public async Task TestXunitNoMethodsInClass()
    {
        var code = """
            using Xunit;
            public class Test$$Class
            {
                public void NotTestMethod() { }
            }
            """;
        await TestXunitAsync(code);
    }
 
    [Fact]
    public async Task TestFindsSelectedXunitMethods()
    {
        var code = """
            using Xunit;
            public class TestClass
            {
                [Fact]
                [|public void TestMethod1() { }
 
                [Fact]
                public void TestMethod2()|] { }
 
                [Theory]
                public void TestMethod3() { }
 
                public void NotTestMethod() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1", "TestMethod2");
    }
 
    #endregion
 
    #region NUnit
 
    [Fact]
    public async Task TestFindsNUnitTestMethod()
    {
        var code = """
            using NUnit.Framework;
            public class TestClass
            {
                [Test]
                public void Test$$Method1() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsNUnitTestAttributeMethod()
    {
        var code = """
            using NUnit.Framework;
            public class TestClass
            {
                [TestAttribute]
                public void Test$$Method1() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsNUnitTheoryMethod()
    {
        var code = """
            using NUnit.Framework;
            public class TestClass
            {
                [Theory]
                public void Test$$Method1() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsNUnitTheoryAttributeMethod()
    {
        var code = """
            using NUnit.Framework;
            public class TestClass
            {
                [TheoryAttribute]
                public void Test$$Method1() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsNUnitTestCaseMethod()
    {
        var code = """
            using NUnit.Framework;
            public class TestClass
            {
                [TestCase]
                public void Test$$Method1() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsNUnitTestCaseAttributeMethod()
    {
        var code = """
            using NUnit.Framework;
            public class TestClass
            {
                [TestCaseAttribute]
                public void Test$$Method1() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsNUnitTestCaseSourceMethod()
    {
        var code = """
            using NUnit.Framework;
            public class TestClass
            {
                public static string s_s = "";
                [TestCaseSource(nameof(s_s))]
                public void Test$$Method1() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsNUnitTestCaseSourceAttributeMethod()
    {
        var code = """
            using NUnit.Framework;
            public class TestClass
            {
                public static string s_s = "";
                [TestCaseSourceAttribute(nameof(s_s))]
                public void Test$$Method1() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsNUnitMethodsInClass()
    {
        var code = """
            using NUnit.Framework;
            public class Test$$Class
            {
                [Test]
                public void TestMethod1() { }
 
                [Theory]
                public void TestMethod2() { }
 
                [TestCase]
                public void TestMethod3() { }
 
                public static string s_s = "";
                [TestCaseSource(nameof(s_s))]
                public void TestMethod4() { }
 
                public void NotTestMethod() { }
            }
            """;
        await TestNUnitAsync(code, "TestMethod1", "TestMethod2", "TestMethod3", "TestMethod4");
    }
 
    #endregion
 
    #region MSTest
 
    [Fact]
    public async Task TestFindsMSTestTestMethod()
    {
        var code = """
            using Microsoft.VisualStudio.TestTools.UnitTesting;
            public class TestClass
            {
                [TestMethod]
                public void Test$$Method1() { }
            }
 
            """;
        await TestMSTestAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsMSTestTestMethodAttribute()
    {
        var code = """
            using Microsoft.VisualStudio.TestTools.UnitTesting;
            public class TestClass
            {
                [TestMethodAttribute]
                public void Test$$Method1() { }
            }
 
            """;
        await TestMSTestAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsMSTestMethodsInClass()
    {
        var code = """
            using Microsoft.VisualStudio.TestTools.UnitTesting;
            public class Test$$Class
            {
                [TestMethod]
                public void TestMethod1() { }
 
                [TestMethod]
                public void TestMethod2() { }
 
                public void NotTestMethod() { }
            }
            """;
        await TestMSTestAsync(code, "TestMethod1", "TestMethod2");
    }
 
    #endregion
 
    [Fact]
    public async Task TestFindsTestMethodInBlockScopedNamespace()
    {
        var code = """
            namespace BlockScoped
            {
                using Xunit;
                public class TestClass
                {
                    [Fact]
                    public void Test$$Method1() { }
                }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsTestMethodInFileScopedNamespace()
    {
        var code = """
            namespace FileScoped;
            using Xunit;
            public class TestClass
            {
                [Fact]
                public void Test$$Method1() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsTestMethodInStruct()
    {
        var code = """
            using Xunit;
            public struct TestClass
            {
                [Fact]
                public void Test$$Method1() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    [Fact]
    public async Task TestFindsTestMethodInPartialClass()
    {
        var code = """
            using Xunit;
            public partial class PartialClass
            {
                [Fact]
                public void Test$$Method1() { }
            }
 
            public partial class PartialClass
            {
                [Fact]
                public void TestMethod2() { }
            }
            """;
        await TestXunitAsync(code, "TestMethod1");
    }
 
    private static Task TestXunitAsync(string code, params string[] expectedTestNames)
    {
        var xunitDefinitions = """
            using System;
            namespace Xunit
            {
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
                public class FactAttribute : Attribute { }
 
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
                public class TheoryAttribute : FactAttribute { }
            }
            """;
 
        return TestAsync(code, xunitDefinitions, expectedTestNames);
    }
 
    private static Task TestXunitMatchAsync(string code, params string[] expectedQualifiedTestNames)
    {
        var xunitDefinitions = """
            using System;
            namespace Xunit
            {
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
                public class FactAttribute : Attribute { }
 
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
                public class TheoryAttribute : FactAttribute { }
            }
            """;
 
        return TestMatchAsync(code, xunitDefinitions, expectedQualifiedTestNames);
    }
 
    private static Task TestNUnitAsync(string code, params string[] expectedTestNames)
    {
        var nunitDefinitions = """
            using System;
            namespace NUnit.Framework
            {
                [AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
                public class TestAttribute : Attribute { }
 
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited=true)]
                public class TheoryAttribute : Attribute { }
 
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited=false)]
                public class TestCaseAttribute : Attribute { }
 
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
                public class TestCaseSourceAttribute : Attribute
                {
                    public TestCaseSourceAttribute(string sourceName) { }
                }
            }
            """;
        return TestAsync(code, nunitDefinitions, expectedTestNames);
    }
 
    private static Task TestMSTestAsync(string code, params string[] expectedTestNames)
    {
        var nunitDefinitions = """
            using System;
            namespace Microsoft.VisualStudio.TestTools.UnitTesting
            {
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
                public class TestMethodAttribute : Attribute { }
            }
            """;
        return TestAsync(code, nunitDefinitions, expectedTestNames);
    }
 
    private static async Task TestAsync(string code, string testAttributeDefinitionsCode, params string[] expectedTestNames)
    {
        var workspace = TestWorkspace.CreateCSharp([code, testAttributeDefinitionsCode]);
 
        var testDocument = workspace.Documents.First();
        var span = testDocument.CursorPosition != null ? new TextSpan(testDocument.CursorPosition.Value, 0) : testDocument.SelectedSpans.Single();
 
        var testMethodFinder = workspace.CurrentSolution.Projects.Single().GetRequiredLanguageService<ITestMethodFinder>();
        var testMethods = await testMethodFinder.GetPotentialTestMethodsAsync(workspace.CurrentSolution.GetRequiredDocument(testDocument.Id), span, CancellationToken.None);
        var testMethodNames = testMethods.Cast<MethodDeclarationSyntax>().Select(m => m.Identifier.Text).ToArray();
 
        AssertEx.Equal(expectedTestNames, testMethodNames);
    }
 
    private static async Task TestMatchAsync(string code, string testAttributeDefinitionsCode, params string[] expectedTestNames)
    {
        var workspace = TestWorkspace.CreateCSharp([code, testAttributeDefinitionsCode]);
 
        var testDocument = workspace.Documents.First();
        var span = testDocument.CursorPosition != null ? new TextSpan(testDocument.CursorPosition.Value, 0) : testDocument.SelectedSpans.Single();
 
        var testMethodFinder = workspace.CurrentSolution.Projects.Single().GetRequiredLanguageService<ITestMethodFinder>();
        var testMethods = await testMethodFinder.GetPotentialTestMethodsAsync(workspace.CurrentSolution.GetRequiredDocument(testDocument.Id), span, CancellationToken.None);
        var semanticModel = await workspace.CurrentSolution.GetRequiredDocument(testDocument.Id).GetRequiredSemanticModelAsync(CancellationToken.None);
 
        List<string> unmatchedTestNames = [];
 
        foreach (var expectedTestName in expectedTestNames)
        {
            var matchFound = testMethods.Any(m => testMethodFinder.IsMatch(semanticModel, m, expectedTestName, CancellationToken.None));
            if (!matchFound)
            {
                unmatchedTestNames.Add(expectedTestName);
            }
        }
 
        Assert.True(unmatchedTestNames.Count == 0, $"Unable to match the following test names: {string.Join(", ", unmatchedTestNames)}");
    }
}