순환복잡도 테스트
순환 복잡도
순환 복잡도(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));
}
}