순환복잡도 테스트

순환복잡도 테스트

순환 복잡도

순환 복잡도(Cyclomatic Complexity)는 소프트웨어 메트릭의 일종으로, 코드의 복잡도를 측정하는 데 사용됩니다. 이는 코드의 제어 흐름 그래프에서 독립적인 경로의 수를 계산하여 결정됩니다. 일반적으로 코드 내의 조건문, 반복문, 예외 처리 등이 많을수록 순환 복잡도가 증가합니다.

순환 복잡도를 테스트해야 하는 이유

  • 유지 보수성 향상: 순환 복잡도가 높은 코드는 이해하기 어렵고, 수정하기도 어렵습니다. 이를 줄이면 코드의 유지 보수성이 향상됩니다.
  • 테스트 용이성: 복잡한 코드는 테스트하기 어려우며, 버그가 발생할 확률이 높습니다. 순환 복잡도를 낮추면 테스트가 용이해지고, 품질이 향상됩니다.
  • 코드 품질 개선: 순환 복잡도가 낮은 코드는 가독성이 높아지고, 협업 시 이해하기 쉬워집니다. 이는 코드 리뷰 및 팀 협업을 원활하게 합니다.

순환 복잡도를 측정하는 방법

순환 복잡도는 다양한 도구와 방법을 통해 측정할 수 있습니다. 여기서는 Roslyn을 사용하여 순환 복잡도를 측정하고, xUnit 테스트 클래스에서 이를 확인하는 방법을 설명합니다.

순환 복잡도 측정을 위한 Roslyn 및 xUnit 설정

NuGet 패키지 설치

dotnet add package Microsoft.CodeAnalysis.CSharp
dotnet add package Microsoft.CodeAnalysis.CSharp.Workspaces

순환복잡도 테스트 코드

ComplexityAnalyzer 클래스

  • 코드의 순환 복잡도를 계산합니다.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
public class ComplexityAnalyzer
{
    public int CalculateComplexity(string code)
    {
        var tree = CSharpSyntaxTree.ParseText(code);
        var root = tree.GetCompilationUnitRoot();
        var compilation = CSharpCompilation.Create("CodeAnalysis")
            .AddSyntaxTrees(tree)
            .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
        var model = compilation.GetSemanticModel(tree);
        var complexity = root.DescendantNodes()
            .OfType<MethodDeclarationSyntax>()
            .Sum(method => CalculateMethodComplexity(method, model));
        return complexity;
    }
    private int CalculateMethodComplexity(MethodDeclarationSyntax method, SemanticModel model)
    {
        var controlFlow = model.AnalyzeControlFlow(method.Body);
        if (controlFlow == null)
            return 1;
        return controlFlow.ExitPoints.Count() + 1;
    }
}

상속 vs 구성

상속과 구성을 모두 사용하여 순환 복잡도 테스트를 구현할 수 있습니다. 각 접근 방식에는 장단점이 있으며, 이를 고려하여 선택할 수 있습니다.

상속 방식

장점:

  • 코드 중복을 줄일 수 있음.
  • 모든 테스트 클래스가 동일한 방식으로 순환 복잡도를 테스트할 수 있음.
  • 간단한 구조로 구현 가능. 단점:
  • 다중 상속을 지원하지 않기 때문에 다른 기본 클래스를 상속받아야 하는 경우 문제가 될 수 있음.
  • 테스트 클래스의 유연성이 떨어질 수 있음.

구성 방식

장점:

  • 테스트 클래스의 유연성이 높음.
  • 다중 상속 문제를 피할 수 있음.
  • 특정 테스트 클래스에서만 순환 복잡도 테스트를 추가할 수 있음. 단점:
  • 코드 중복이 발생할 수 있음.
  • 구성 객체를 명시적으로 초기화해야 함.

추상클래스 상속 방식

TestBase 클래스

  • 공통된 테스트 로직을 포함하는 추상 테스트 기반 클래스입니다.
  • 테스트 대상 클래스의 소스 코드를 동적으로 가져옵니다.
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Xunit;
public abstract class ComplexityTestBase<T>
{
    private readonly ComplexityAnalyzer _complexityAnalyzer = new ComplexityAnalyzer();
    [Fact]
    public void TestCyclomaticComplexity()
    {
        // Arrange
        var code = GetCodeToAnalyze();
        // Act
        var complexity = _complexityAnalyzer.CalculateComplexity(code);
        // Assert
        Assert.True(complexity <= 10, $"Cyclomatic complexity is too high: {complexity}");
    }
    private string GetCodeToAnalyze()
    {
        var type = typeof(T);
        var assembly = Assembly.GetAssembly(type);
        var resourceName = assembly?.GetManifestResourceNames().FirstOrDefault(n => n.EndsWith(type.Name + ".cs"));
        if (resourceName == null)
            throw new InvalidOperationException($"Could not find embedded resource for {type.Name}.cs");
        using (var stream = assembly?.GetManifestResourceStream(resourceName))
        {
            if (stream != null)
            {
                using (var reader = new StreamReader(stream))
                    return reader.ReadToEnd();
            }
            else 
                return string.Empty;
        }
    }
}

LogManagerTests 클래스

  • TestBase 클래스를 상속받아 순환 복잡도와 기존의 로깅 테스트를 수행합니다.
public class LogManagerTests : ComplexityTestBase<LogManagerTests> 
{	
}

구성 방식

구성 방식을 사용하여 순환 복잡도 테스트를 구현하는 방법을 설명합니다.

ComplexityTestHelper 클래스

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Xunit;
public class CyclomaticComplexityTestHelper
{
    private readonly ComplexityAnalyzer _complexityAnalyzer = new ComplexityAnalyzer();
    public void TestCyclomaticComplexity(Type typeUnderTest)
    {
        // Arrange
        var code = GetCodeToAnalyze(typeUnderTest);
        // Act
        var complexity = _complexityAnalyzer.CalculateComplexity(code);
        // Assert
        Assert.True(complexity <= 10, $"Cyclomatic complexity is too high: {complexity}");
    }
    private string GetCodeToAnalyze(Type typeUnderTest)
    {
        var assembly = Assembly.GetAssembly(typeUnderTest);
        var resourceName = assembly.GetManifestResourceNames().FirstOrDefault(n => n.EndsWith(typeUnderTest.Name + ".cs"));
        if (resourceName == null)
            throw new InvalidOperationException($"Could not find embedded resource for {typeUnderTest.Name}.cs");
        using (var stream = assembly.GetManifestResourceStream(resourceName))
        using (var reader = new StreamReader(stream))
        {
            return reader.ReadToEnd();
        }
    }
}

LogManagerTests 클래스

using Serilog;
using Serilog.Events;
using Serilog.Sinks.TestCorrelator;
using System;
using System.IO;
using System.Linq;
using Xunit;
public class LogManagerTests
{
    private readonly CyclomaticComplexityTestHelper _complexityTestHelper = new CyclomaticComplexityTestHelper();
    [Fact]
    public void Test_LogManagerCyclomaticComplexity()
    {
        _complexityTestHelper.TestCyclomaticComplexity(typeof(LogManager));
    }
}