İlkay İlknur

Source Generatorlara Nasıl Unit Test Yazarız?

June 07, 2021

Bugüne kadar source generatorlar ile ilgili pek çok konuyu blogdaki yazılarda işledik. Hatta geçtiğimiz haftalarda bu konuyla ilgili Teknolot topluluğunun düzenlediği meetupta da detaylı olarak source generatorları anlatma fırsatı yakaladım. Gerek o meetupta gerekse blogdaki yazılarda bahsetmek isteyip ancak fırsat bulamadığım tek bir konu kaldı. O da source generatorlara nasıl unit test yazarız konusu.

Source generatorları geliştirme aşamasında Visual Studio üzerinden manuel test etmek biraz zor olabiliyor. Bu nedenle unit test yazarak unit testler üzerinden testleri yürütmek bizler için biraz daha kolay bir opsiyon olabilir. Tabi ki unit testlerin anlamı ve değeri Visual Studio üzerinden yapılan manuel testlerden çok daha farklı ancak manuel test yapmanın da zor olabildiği bir ortamda unit test yazmak çok daha mantıklı bir çözüm olarak önümüze gelebilir.

Şimdi gelelim source generatorlar için nasıl unit test yazacağız kısmına. Örnek olması açısından ben source generator meetupında demosunu yaptığım INotifyPropertyChanged implementasyonunu gerçekleştiren kodu test edeceğim. Bu source generatorın benzerine buradan da ulaşabilirsiniz.

Source generatorların unit testini yazmanın aslında normal unit test yazmaktan pek bir farkı yok. Derlenecek kodu veriyoruz, o kod üzerinde source generator çalışıyor ve çıktı üzerinde karşılaştırmalar yapıyoruz.

Temel anlamda bir source generator unit test şablonu aşağıdaki gibi olmakta.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using MyTeknolotGenerator;
using NUnit.Framework;
using System.Reflection;

namespace Generator.Tests
{
    public class Tests
    {
        [Test]
        public void Test1()
        {
            //Arrange
            var code = @"
namespace Demo
{
    public partial class MyVM
    {
        [Notifier.AutoNotify]
        private string _name;

        [Notifier.AutoNotify]
        private int _clickCount;
    }
}";
            var generator = new AutoNotifyGenerator();
            var driver = CSharpGeneratorDriver.Create(generator);

            //Act
            driver.RunGeneratorsAndUpdateCompilation(CreateCompilation(code), out var updatedCompilation, out var diagnostics);

            //Assert

        }

        private static Compilation CreateCompilation(string source)
            => CSharpCompilation.Create("compilation",
                new[] { CSharpSyntaxTree.ParseText(source) },
                new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) },
                new CSharpCompilationOptions(OutputKind.ConsoleApplication));
    }
}

Unit test yazarken tabi ki esas görevi CSharpGeneratorDriver üstleniyor. Generator instance'ı yaratıp CSharpGeneratorDriver'a parametre olarak geçtiğimizde CSharpGeneratorDriver üzerinden bir takım operasyonlar yaparak source generatorları execute ettirebiliyoruz. Bunlardan ilki RunGeneratorsAndUpdateCompilation metodunu kullanarak çalıştırma ve out parametrelerinden gelen yeni compilation ve diagnostics instancelarını elde etmek ve sonrasında bu objectler üzerinden kontroller yapmak.

//Assert

Assert.IsTrue(diagnostics.IsEmpty);
Assert.AreEqual(3, updatedCompilation.SyntaxTrees.Count());

var generatedSyntaxTree = updatedCompilation.SyntaxTrees.Last();
var classDeclaration = generatedSyntaxTree.GetRoot().DescendantNodes()
    .OfType<ClassDeclarationSyntax>().SingleOrDefault();
Assert.AreEqual("MyVM", classDeclaration.Identifier.Text);

Yukarıdaki gibi dilediğiniz kontrolleri out parametresi üzerinden gelen objectler üzerinden yapabilirsiniz. Bir diğer opsiyon da GetRunResult metodunu çağırarak doğrudan result üzerinden karşılaştırmaları yapmak. Bu şekilde ilerlediğinizde oluşturulan source kodlar üzerinden de karşılaştırma yapabilirsiniz.

public class Tests
    {
        [Test]
        public void Test1()
        {
            //Arrange
            var code = @"
namespace Demo
{
    public partial class MyVM
    {
        [Notifier.AutoNotify]
        private string _name;

        [Notifier.AutoNotify]
        private int _clickCount;
    }
}";
            var generator = new AutoNotifyGenerator();
            GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);

            //Act
            driver = driver.RunGeneratorsAndUpdateCompilation(CreateCompilation(code), out var _, out var _);
            var result = driver.GetRunResult();
            //Assert

            Assert.IsTrue(result.Diagnostics.IsEmpty);
            Assert.AreEqual(2, result.GeneratedTrees.Count());
            Assert.AreEqual(generator, result.Results.First().Generator);
            var generatedSource = result.Results.First().GeneratedSources[1].SourceText.ToString();
            var expectedCode = @"
namespace Demo
{
    using System.ComponentModel;
    public partial class MyVM : INotifyPropertyChanged
    {
        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
        public string name
        {
                get { return _name;}
                set { 
                     _name=value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(name)));
                    }
        }

        public int clickCount
        {
                get { return _clickCount;}
                set { 
                     _clickCount=value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(clickCount)));
                    }
        }
}}
";
            Assert.AreEqual(expectedCode, generatedSource);
        }

        private static Compilation CreateCompilation(string source)
            => CSharpCompilation.Create("compilation",
                new[] { CSharpSyntaxTree.ParseText(source) },
                new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) },
                new CSharpCompilationOptions(OutputKind.ConsoleApplication));
    }

Burada dikkat edilmesi gereken nokta ise GeneratorDriver'ın immutable olması. Bu nedenle generatorların çağırılması sonrasında dönen instance'ın tekrardan driver variable'ına atanması gerekiyor. Böylece bir sonraki adımda GetRunResult metodunu çağırıp birleştirilmiş olan sonuçları alıp kontrol edebiliyoruz. Birden fazla generatorın çalışması gibi senaryoları da yine bu şekilde test edebiliriz.

Bu yazıda source generatorların unit testlerinin yazılması konusunu işledik. Source generator tarafına bulaştığınızda ve manuel test yapmanın bazı zorluklarını gördüğünüzde umarım bu yazı sizin için faydalı olur. 😃

Bir sonraki yazıda görüşmek üzere.

Kaynak: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md