Files
Amaury Levé 948a9a52ef Add exp-mock-usage-analysis skill and evaluation (#483)
* Add exp-mock-usage-analysis skill and evaluation

New experimental skill that detects unused, redundant, or unnecessary
mocks in .NET test suites. Covers Moq, NSubstitute, FakeItEasy, and
manual test doubles.

- SKILL.md with 6-step workflow: gather code, classify mocks, detect
  anti-patterns, incorporate runtime data, generate recommendations,
  report findings
- eval.yaml with 10 scenarios: trivial type mocking, unused setups,
  redundant configs, stable utility mocks, well-placed mocks (no false
  positives), NSubstitute support, FakeItEasy support, setup sprawl,
  and 2 non-activation boundary tests
- Fixture files for each scenario with production + test code
- CODEOWNERS entry for @dotnet/dotnet-testing

* Remove no-headroom scenarios, increase NSubstitute timeout

Remove 4 scenarios where baseline already scores 5.0/5 (model handles
these well without skill guidance):
- Detect mocking of DTOs, records, and enums
- Recognize well-placed mocks without inventing false positives
- Decline when asked about non-mock test anti-patterns
- Decline mock framework migration request

Increase NSubstitute scenario timeout from 120s to 180s to avoid
baseline timeouts impacting scoring.

Remaining 6 scenarios all show positive skill impact (3→5, 3→4, etc).

* Document areas the model handles well without the skill

Add a subsection under 'When Not to Use' listing mock-related tasks
where the base model already scores 5.0/5 without skill guidance:
- Mocking trivial types (DTOs, records, enums)
- Recognizing well-placed mocks (no false positives)

* Fix md

* Improve
2026-04-02 10:03:17 +00:00

119 lines
5.1 KiB
C#

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Extensions.Logging;
using Moq;
using ReportGen;
namespace ReportGen.Tests;
[TestClass]
public sealed class ReportGeneratorTests
{
[TestMethod]
public void GenerateSalesReport_Html_SavesReport()
{
// Excessive mock setup — 20+ lines of configuration to test one method call
var mockDataSource = new Mock<IDataSource>();
var mockFormatter = new Mock<IReportFormatter>();
var mockStorage = new Mock<IReportStorage>();
var mockCache = new Mock<ICacheService>();
var mockLogger = new Mock<ILogger<ReportGenerator>>();
var from = new DateTime(2025, 1, 1);
var to = new DateTime(2025, 1, 31);
var salesData = new List<SalesRecord>
{
new("PROD-1", 100m, "USD", new DateTime(2025, 1, 5)),
new("PROD-1", 150m, "USD", new DateTime(2025, 1, 10)),
new("PROD-2", 200m, "USD", new DateTime(2025, 1, 15))
};
mockDataSource.Setup(d => d.GetSalesData(from, to)).Returns(salesData);
// Unused: GetCustomers is never called during GenerateSalesReport
mockDataSource.Setup(d => d.GetCustomers()).Returns(new List<CustomerRecord>
{
new(1, "Alice", "US"),
new(2, "Bob", "EU")
});
// Unused: GetProducts is never called during GenerateSalesReport
mockDataSource.Setup(d => d.GetProducts()).Returns(new List<ProductRecord>
{
new("PROD-1", "Widget", "Hardware", 50m),
new("PROD-2", "Gadget", "Electronics", 100m)
});
// Unused: exchange rate is never called for this report type
mockDataSource.Setup(d => d.GetExchangeRate("USD", "EUR")).Returns(0.92m);
mockDataSource.Setup(d => d.GetExchangeRate("USD", "GBP")).Returns(0.79m);
mockFormatter.Setup(f => f.FormatAsHtml(It.IsAny<ReportData>())).Returns("<html>report</html>");
// Unused: PDF and CSV formatters not called for HTML format
mockFormatter.Setup(f => f.FormatAsPdf(It.IsAny<ReportData>())).Returns("pdf-bytes");
mockFormatter.Setup(f => f.FormatAsCsv(It.IsAny<ReportData>())).Returns("col1,col2");
mockStorage.Setup(s => s.Save(It.IsAny<string>(), It.IsAny<string>(), "html")).Returns("RPT-001");
// Unused: GetLatest never called during GenerateSalesReport
mockStorage.Setup(s => s.GetLatest(It.IsAny<string>())).Returns("old-report");
// Unused: cache is never used during GenerateSalesReport
mockCache.Setup(c => c.Get<ReportData>(It.IsAny<string>())).Returns((ReportData?)null);
mockCache.Setup(c => c.Set(It.IsAny<string>(), It.IsAny<ReportData>(), It.IsAny<TimeSpan>()));
mockCache.Setup(c => c.Invalidate(It.IsAny<string>()));
var generator = new ReportGenerator(
mockDataSource.Object,
mockFormatter.Object,
mockStorage.Object,
mockCache.Object,
mockLogger.Object);
var result = generator.GenerateSalesReport(from, to, "html");
Assert.AreEqual("RPT-001", result);
}
[TestMethod]
public void GenerateSalesReport_Csv_SavesReport()
{
// Exact same sprawling setup repeated for a different format
var mockDataSource = new Mock<IDataSource>();
var mockFormatter = new Mock<IReportFormatter>();
var mockStorage = new Mock<IReportStorage>();
var mockCache = new Mock<ICacheService>();
var mockLogger = new Mock<ILogger<ReportGenerator>>();
var from = new DateTime(2025, 2, 1);
var to = new DateTime(2025, 2, 28);
var salesData = new List<SalesRecord>
{
new("PROD-3", 300m, "USD", new DateTime(2025, 2, 5)),
};
mockDataSource.Setup(d => d.GetSalesData(from, to)).Returns(salesData);
mockDataSource.Setup(d => d.GetCustomers()).Returns(new List<CustomerRecord>());
mockDataSource.Setup(d => d.GetProducts()).Returns(new List<ProductRecord>());
mockDataSource.Setup(d => d.GetExchangeRate(It.IsAny<string>(), It.IsAny<string>())).Returns(1.0m);
mockFormatter.Setup(f => f.FormatAsHtml(It.IsAny<ReportData>())).Returns("<html></html>");
mockFormatter.Setup(f => f.FormatAsPdf(It.IsAny<ReportData>())).Returns("pdf");
mockFormatter.Setup(f => f.FormatAsCsv(It.IsAny<ReportData>())).Returns("Product,Amount\nPROD-3,300");
mockStorage.Setup(s => s.Save(It.IsAny<string>(), It.IsAny<string>(), "csv")).Returns("RPT-002");
mockStorage.Setup(s => s.GetLatest(It.IsAny<string>())).Returns((string?)null);
mockCache.Setup(c => c.Get<ReportData>(It.IsAny<string>())).Returns((ReportData?)null);
mockCache.Setup(c => c.Set(It.IsAny<string>(), It.IsAny<ReportData>(), It.IsAny<TimeSpan>()));
var generator = new ReportGenerator(
mockDataSource.Object,
mockFormatter.Object,
mockStorage.Object,
mockCache.Object,
mockLogger.Object);
var result = generator.GenerateSalesReport(from, to, "csv");
Assert.AreEqual("RPT-002", result);
}
}