# SkillPatch skill: mstest-skill

This skill generates MSTest unit tests in C# using Microsoft's built-in .NET testing framework. It provides actionable patterns for basic tests, data-driven tests, assertions, lifecycle hooks, and test categories, along with setup and run commands. It also references a deeper playbook covering CI/CD, integration testing, and production best practices.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/mstest-skill
curl -sSL https://skillpatch.dev/install_skill/mstest-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: mstest-skill
description: >
  Generates MSTest tests in C#. Microsoft's built-in testing framework for .NET.
  Use when user mentions "MSTest", "[TestMethod]", "[TestClass]",
  "Assert.AreEqual". Triggers on: "MSTest", "[TestMethod]", "[TestClass]",
  "Microsoft test framework".
languages:
  - C#
category: unit-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# MSTest Testing Skill

## Core Patterns

### Basic Test

```csharp
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class CalculatorTests
{
    private Calculator _calc;

    [TestInitialize]
    public void SetUp() => _calc = new Calculator();

    [TestMethod]
    public void Add_TwoNumbers_ReturnsSum()
    {
        Assert.AreEqual(5, _calc.Add(2, 3));
    }

    [TestMethod]
    [ExpectedException(typeof(DivideByZeroException))]
    public void Divide_ByZero_Throws()
    {
        _calc.Divide(10, 0);
    }
}
```

### Data-Driven Tests

```csharp
[DataTestMethod]
[DataRow(2, 3, 5)]
[DataRow(-1, 1, 0)]
[DataRow(0, 0, 0)]
public void Add_Parameterized(int a, int b, int expected)
{
    Assert.AreEqual(expected, _calc.Add(a, b));
}

[DataTestMethod]
[DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)]
public void Add_DynamicData(int a, int b, int expected)
{
    Assert.AreEqual(expected, _calc.Add(a, b));
}

private static IEnumerable<object[]> GetTestData()
{
    yield return new object[] { 1, 2, 3 };
    yield return new object[] { 10, -5, 5 };
}
```

### Assertions

```csharp
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(unexpected, actual);
Assert.IsTrue(condition);
Assert.IsFalse(condition);
Assert.IsNull(obj);
Assert.IsNotNull(obj);
Assert.IsInstanceOfType(obj, typeof(MyClass));
Assert.ThrowsException<ArgumentException>(() => Method());
CollectionAssert.Contains(list, item);
CollectionAssert.AreEquivalent(expected, actual);
StringAssert.Contains(str, "substring");
StringAssert.StartsWith(str, "prefix");
StringAssert.Matches(str, new Regex(@"\d+"));
```

### Lifecycle

```
[AssemblyInitialize]  → Once per assembly (static)
[ClassInitialize]     → Once per class (static)
[TestInitialize]      → Before each test
[TestMethod]          → Test
[TestCleanup]         → After each test
[ClassCleanup]        → Once after class (static)
[AssemblyCleanup]     → Once after assembly (static)
```

### Categories

```csharp
[TestMethod, TestCategory("Smoke")]
public void QuickTest() { }

[TestMethod, Ignore("Bug #456")]
public void SkippedTest() { }

[TestMethod, Timeout(5000)]
public void TimedTest() { }
```

## Setup: `dotnet add package MSTest.TestFramework MSTest.TestAdapter Microsoft.NET.Test.Sdk`
## Run: `dotnet test` or `dotnet test --filter "TestCategory=Smoke"`

## Deep Patterns

See `reference/playbook.md` for production-grade patterns:

| Section | What You Get |
|---------|-------------|
| §1 Project Setup | .csproj deps, .runsettings with parallel + coverage config |
| §2 Test Patterns | TestMethod, DataTestMethod, DynamicData, exception testing |
| §3 FluentAssertions | AssertionScope, async exceptions, collection assertions |
| §4 Class & Assembly Initialize | Testcontainers, shared expensive setup, global config |
| §5 WebApplicationFactory | API integration tests with in-memory DB |
| §6 Bogus Test Data | Faker patterns for realistic data generation |
| §7 TestContext & Logging | Diagnostic output, categories, timeouts |
| §8 CI/CD Integration | GitHub Actions with coverage reporting and thresholds |
| §9 Debugging Table | 12 common problems with causes and fixes |
| §10 Best Practices | 14-item production checklist |

````


### `reference/advanced-patterns.md`

````markdown
# MSTest — Advanced Patterns & Playbook

## Data-Driven Tests

```csharp
[TestClass]
public class CalculatorTests
{
    [DataTestMethod]
    [DataRow(1, 2, 3)]
    [DataRow(-1, -1, -2)]
    [DataRow(0, 0, 0)]
    public void Add(int a, int b, int expected)
        => Assert.AreEqual(expected, Calculator.Add(a, b));

    [TestMethod]
    [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)]
    public void Process(string input, string expected)
        => Assert.AreEqual(expected, Processor.Run(input));

    static IEnumerable<object[]> GetTestData()
    {
        yield return new object[] { "hello", "HELLO" };
        yield return new object[] { "world", "WORLD" };
    }

    // CSV data source
    [DataTestMethod]
    [CsvDataSource("TestData/users.csv")]
    public void ImportUsers(string name, string email) { /* ... */ }
}
```

## Test Lifecycle

```csharp
[TestClass]
public class IntegrationTests
{
    static HttpClient _client;

    [ClassInitialize]
    public static void ClassInit(TestContext context)
    {
        var factory = new WebApplicationFactory<Program>();
        _client = factory.CreateClient();
    }

    [ClassCleanup]
    public static void ClassCleanup() => _client?.Dispose();

    [TestInitialize]
    public void TestInit() { /* before each */ }

    [TestCleanup]
    public void TestClean() { /* after each */ }

    [TestMethod]
    public async Task GetUsers_ReturnsOk()
    {
        var response = await _client.GetAsync("/api/users");
        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    }
}
```

## MSTest v3 Source Generators

```csharp
// Auto-generated assertions with MSTest v3
[TestClass]
public partial class UserTests
{
    [TestMethod]
    public void UserCreation()
    {
        var user = new User("Alice", 30);
        Assert.AreEqual("Alice", user.Name);
        Assert.IsTrue(user.Age > 0);
        Assert.IsNotNull(user.Id);
        StringAssert.Contains(user.Email, "@");
        CollectionAssert.Contains(user.Roles, "default");
    }
}
```

## Anti-Patterns

- ❌ `[ClassInitialize]` without `static` — MSTest requires it
- ❌ `Assert.IsTrue(a.Equals(b))` — use `Assert.AreEqual(a, b)`
- ❌ Missing `[TestCategory]` — makes filtering in CI difficult
- ❌ `[ExpectedException]` attribute — use `Assert.ThrowsException<T>()` instead

````


### `reference/playbook.md`

````markdown
# MSTest — Advanced Playbook

## §1 Project Setup & Configuration

### .csproj Dependencies
```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
    <PackageReference Include="MSTest.TestAdapter" Version="3.2.0" />
    <PackageReference Include="MSTest.TestFramework" Version="3.2.0" />
    <PackageReference Include="FluentAssertions" Version="6.12.0" />
    <PackageReference Include="Moq" Version="4.20.70" />
    <PackageReference Include="NSubstitute" Version="5.1.0" />
    <PackageReference Include="Bogus" Version="35.4.0" />
    <PackageReference Include="Testcontainers" Version="3.7.0" />
    <PackageReference Include="Testcontainers.PostgreSql" Version="3.7.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
    <PackageReference Include="coverlet.collector" Version="6.0.1" />
  </ItemGroup>
</Project>
```

### .runsettings
```xml
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <MSTest>
    <Parallelize>
      <Workers>4</Workers>
      <Scope>ClassLevel</Scope>
    </Parallelize>
    <MapInconclusiveToFailed>true</MapInconclusiveToFailed>
    <TreatDiscoveryWarningsAsErrors>true</TreatDiscoveryWarningsAsErrors>
  </MSTest>
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="XPlat code coverage">
        <Configuration>
          <Format>cobertura</Format>
          <Exclude>[*]*.Migrations.*</Exclude>
          <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute</ExcludeByAttribute>
          <SingleHit>false</SingleHit>
          <IncludeTestAssembly>false</IncludeTestAssembly>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
</RunSettings>
```

---

## §2 Test Patterns — TestMethod & DataTestMethod

### Basic Test Structure
```csharp
[TestClass]
public class OrderServiceTests
{
    private Mock<IOrderRepository> _mockRepo = null!;
    private Mock<IPaymentGateway> _mockPayment = null!;
    private Mock<IEmailService> _mockEmail = null!;
    private OrderService _service = null!;

    [TestInitialize]
    public void Setup()
    {
        _mockRepo = new Mock<IOrderRepository>();
        _mockPayment = new Mock<IPaymentGateway>();
        _mockEmail = new Mock<IEmailService>();
        _service = new OrderService(
            _mockRepo.Object,
            _mockPayment.Object,
            _mockEmail.Object
        );
    }

    [TestCleanup]
    public void Cleanup()
    {
        // Verify no unexpected calls
        _mockRepo.VerifyNoOtherCalls();
    }

    [TestMethod]
    public async Task PlaceOrder_ValidOrder_ReturnsOrderId()
    {
        // Arrange
        var items = new List<OrderItem>
        {
            new("SKU-001", 2, 29.99m),
            new("SKU-002", 1, 49.99m)
        };
        _mockPayment.Setup(p => p.ChargeAsync(It.IsAny<decimal>(), It.IsAny<string>()))
            .ReturnsAsync(new PaymentResult { Success = true, TransactionId = "TXN-123" });
        _mockRepo.Setup(r => r.SaveAsync(It.IsAny<Order>()))
            .ReturnsAsync(new Order { Id = 42 });

        // Act
        var result = await _service.PlaceOrderAsync(items, "card_abc");

        // Assert
        Assert.IsNotNull(result);
        Assert.AreEqual(42, result.Id);
        Assert.AreEqual("TXN-123", result.TransactionId);
        _mockPayment.Verify(p => p.ChargeAsync(109.97m, "card_abc"), Times.Once);
        _mockEmail.Verify(e => e.SendOrderConfirmationAsync(42), Times.Once);
    }

    [TestMethod]
    [ExpectedException(typeof(InsufficientStockException))]
    public async Task PlaceOrder_OutOfStock_ThrowsException()
    {
        _mockRepo.Setup(r => r.CheckStockAsync("SKU-001"))
            .ReturnsAsync(0);

        var items = new List<OrderItem> { new("SKU-001", 5, 10m) };
        await _service.PlaceOrderAsync(items, "card_abc");
    }

    [TestMethod]
    public async Task PlaceOrder_PaymentFails_DoesNotSaveOrder()
    {
        _mockPayment.Setup(p => p.ChargeAsync(It.IsAny<decimal>(), It.IsAny<string>()))
            .ReturnsAsync(new PaymentResult { Success = false, Error = "Declined" });

        var items = new List<OrderItem> { new("SKU-001", 1, 10m) };

        await Assert.ThrowsExceptionAsync<PaymentFailedException>(
            () => _service.PlaceOrderAsync(items, "card_abc"));

        _mockRepo.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Never);
    }
}
```

### DataTestMethod with DataRow
```csharp
[TestClass]
public class PricingCalculatorTests
{
    private readonly PricingCalculator _calculator = new();

    [DataTestMethod]
    [DataRow(100.0, 0.0, 100.0, DisplayName = "No discount")]
    [DataRow(100.0, 10.0, 90.0, DisplayName = "10% discount")]
    [DataRow(100.0, 100.0, 0.0, DisplayName = "100% discount")]
    [DataRow(0.0, 50.0, 0.0, DisplayName = "Zero price")]
    public void CalculateDiscount_ReturnsCorrectPrice(
        double price, double discountPercent, double expected)
    {
        var result = _calculator.ApplyDiscount((decimal)price, (decimal)discountPercent);
        Assert.AreEqual((decimal)expected, result);
    }

    [DataTestMethod]
    [DataRow(-1.0, DisplayName = "Negative price")]
    [DataRow(double.NaN, DisplayName = "NaN price")]
    public void CalculateDiscount_InvalidPrice_Throws(double price)
    {
        Assert.ThrowsException<ArgumentException>(
            () => _calculator.ApplyDiscount((decimal)price, 10m));
    }
}
```

### DynamicData for Complex Test Data
```csharp
[TestClass]
public class ValidationTests
{
    public static IEnumerable<object[]> InvalidUserData =>
        new List<object[]>
        {
            new object[] { "", "alice@test.com", "Name is required" },
            new object[] { "Alice", "", "Email is required" },
            new object[] { "Alice", "not-an-email", "Invalid email format" },
            new object[] { new string('A', 256), "a@b.com", "Name exceeds max length" },
        };

    [DataTestMethod]
    [DynamicData(nameof(InvalidUserData))]
    public void Validate_InvalidInput_ReturnsError(
        string name, string email, string expectedError)
    {
        var validator = new UserValidator();
        var result = validator.Validate(new UserDto { Name = name, Email = email });

        Assert.IsFalse(result.IsValid);
        CollectionAssert.Contains(result.Errors.Select(e => e.Message).ToList(), expectedError);
    }
}
```

---

## §3 FluentAssertions Integration

```csharp
using FluentAssertions;
using FluentAssertions.Execution;

[TestClass]
public class UserServiceFluentTests
{
    [TestMethod]
    public async Task GetUser_ReturnsFullProfile()
    {
        var user = await _service.GetByIdAsync(1);

        user.Should().NotBeNull();
        user!.Name.Should().Be("Alice");
        user.Email.Should().EndWith("@example.com");
        user.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
        user.Roles.Should().Contain("admin").And.HaveCount(2);
    }

    [TestMethod]
    public void GetUsers_ReturnsSortedByName()
    {
        var users = _service.GetAll();

        users.Should().BeInAscendingOrder(u => u.Name);
        users.Should().OnlyContain(u => u.IsActive);
        users.Should().HaveCountGreaterThanOrEqualTo(1);
    }

    [TestMethod]
    public async Task CreateUser_ShouldThrowWithDetails()
    {
        var act = () => _service.CreateAsync(new UserDto { Name = "" });

        await act.Should().ThrowAsync<ValidationException>()
            .WithMessage("*Name*required*")
            .Where(e => e.Errors.Count > 0);
    }

    [TestMethod]
    public void AssertMultipleProperties()
    {
        var order = _service.GetOrder(1);

        using (new AssertionScope())
        {
            order.Status.Should().Be("Completed");
            order.Total.Should().BeGreaterThan(0);
            order.Items.Should().NotBeEmpty();
            order.ShippedAt.Should().NotBeNull();
        }
    }
}
```

---

## §4 Assembly & Class Initialize

### Shared Expensive Setup
```csharp
[TestClass]
public class DatabaseIntegrationTests
{
    private static PostgreSqlContainer _postgres = null!;
    private static string _connectionString = null!;

    [ClassInitialize]
    public static async Task ClassSetup(TestContext context)
    {
        _postgres = new PostgreSqlBuilder()
            .WithImage("postgres:16-alpine")
            .WithDatabase("testdb")
            .Build();
        await _postgres.StartAsync();
        _connectionString = _postgres.GetConnectionString();

        // Run migrations
        using var conn = new NpgsqlConnection(_connectionString);
        await conn.OpenAsync();
        var evolve = new Evolve.Evolve(conn, msg => context.WriteLine(msg))
        {
            Locations = new[] { "db/migrations" },
        };
        evolve.Migrate();
    }

    [ClassCleanup]
    public static async Task ClassCleanup()
    {
        await _postgres.DisposeAsync();
    }

    [TestInitialize]
    public async Task TestSetup()
    {
        // Clean tables between tests
        using var conn = new NpgsqlConnection(_connectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync("TRUNCATE users, orders CASCADE");
    }

    [TestMethod]
    public async Task SaveAndRetrieveUser()
    {
        var repo = new UserRepository(_connectionString);
        var user = new User { Name = "Alice", Email = "alice@test.com" };

        var saved = await repo.SaveAsync(user);
        var retrieved = await repo.GetByIdAsync(saved.Id);

        Assert.IsNotNull(retrieved);
        Assert.AreEqual("Alice", retrieved.Name);
    }
}
```

### AssemblyInitialize for Global Setup
```csharp
[TestClass]
public class GlobalSetup
{
    [AssemblyInitialize]
    public static void AssemblyInit(TestContext context)
    {
        Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Test");
        // Global one-time setup
    }

    [AssemblyCleanup]
    public static void AssemblyCleanup()
    {
        // Global teardown
    }
}
```

---

## §5 Integration Testing with WebApplicationFactory

```csharp
[TestClass]
public class UsersApiTests
{
    private static WebApplicationFactory<Program> _factory = null!;
    private static HttpClient _client = null!;

    [ClassInitialize]
    public static void ClassSetup(TestContext context)
    {
        _factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // Replace real DB with in-memory
                    var descriptor = services.SingleOrDefault(
                        d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
                    if (descriptor != null) services.Remove(descriptor);

                    services.AddDbContext<AppDbContext>(options =>
                        options.UseInMemoryDatabase("TestDb"));

                    // Seed test data
                    var sp = services.BuildServiceProvider();
                    using var scope = sp.CreateScope();
                    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                    db.Database.EnsureCreated();
                    db.Users.Add(new User { Id = 1, Name = "Alice", Email = "alice@test.com" });
                    db.SaveChanges();
                });
            });

        _client = _factory.CreateClient();
        _client.DefaultRequestHeaders.Add("X-Api-Key", "test-key");
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        _client.Dispose();
        _factory.Dispose();
    }

    [TestMethod]
    public async Task GetUsers_ReturnsOk()
    {
        var response = await _client.GetAsync("/api/users");

        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var users = await response.Content.ReadFromJsonAsync<List<UserDto>>();
        users.Should().NotBeEmpty();
    }

    [TestMethod]
    public async Task CreateUser_ReturnsCreated()
    {
        var payload = new { Name = "Bob", Email = "bob@test.com" };
        var response = await _client.PostAsJsonAsync("/api/users", payload);

        response.StatusCode.Should().Be(HttpStatusCode.Created);
        var user = await response.Content.ReadFromJsonAsync<UserDto>();
        user!.Name.Should().Be("Bob");
        response.Headers.Location.Should().NotBeNull();
    }

    [TestMethod]
    public async Task CreateUser_InvalidData_ReturnsBadRequest()
    {
        var payload = new { Name = "", Email = "not-email" };
        var response = await _client.PostAsJsonAsync("/api/users", payload);

        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
        var problem = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
        problem!.Errors.Should().ContainKey("Name");
    }
}
```

---

## §6 Test Data Generation with Bogus

```csharp
using Bogus;

[TestClass]
public class BogusDataTests
{
    private static readonly Faker<UserDto> UserFaker = new Faker<UserDto>()
        .RuleFor(u => u.Name, f => f.Person.FullName)
        .RuleFor(u => u.Email, f => f.Internet.Email())
        .RuleFor(u => u.Phone, f => f.Phone.PhoneNumber())
        .RuleFor(u => u.Address, f => f.Address.FullAddress())
        .RuleFor(u => u.CreatedAt, f => f.Date.Past(1));

    private static readonly Faker<OrderDto> OrderFaker = new Faker<OrderDto>()
        .RuleFor(o => o.Items, f => f.Make(f.Random.Int(1, 5), () => new OrderItem
        {
            Sku = f.Commerce.Ean13(),
            Quantity = f.Random.Int(1, 10),
            Price = decimal.Parse(f.Commerce.Price()),
        }))
        .RuleFor(o => o.Status, f => f.PickRandom("Pending", "Shipped", "Delivered"));

    [TestMethod]
    public async Task BulkCreate_HandlesLargeDataSets()
    {
        var users = UserFaker.Generate(100);

        var result = await _service.BulkCreateAsync(users);

        result.SuccessCount.Should().Be(100);
        result.Errors.Should().BeEmpty();
    }

    [TestMethod]
    public void OrderTotal_CalculatesCorrectly()
    {
        // Seeded for reproducibility
        var order = OrderFaker.UseSeed(12345).Generate();

        var total = _calculator.CalculateTotal(order);

        total.Should().Be(order.Items.Sum(i => i.Price * i.Quantity));
    }
}
```

---

## §7 TestContext & Logging

```csharp
[TestClass]
public class DiagnosticTests
{
    public TestContext TestContext { get; set; } = null!;

    [TestMethod]
    public async Task LongRunningOperation_CompletesInTime()
    {
        var sw = Stopwatch.StartNew();

        var result = await _service.ProcessBatchAsync(1000);

        sw.Stop();
        TestContext.WriteLine($"Batch processing took {sw.ElapsedMilliseconds}ms");
        TestContext.WriteLine($"Processed {result.Count} items");

        result.Count.Should().Be(1000);
        sw.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(5));
    }

    [TestMethod]
    [TestCategory("Integration")]
    [TestProperty("Priority", "High")]
    [Timeout(30000)]
    public async Task ExternalApi_RespondsWithinSla()
    {
        TestContext.WriteLine($"Test running at {DateTime.UtcNow}");
        var response = await _client.GetAsync("/api/health");
        TestContext.WriteLine($"Response status: {response.StatusCode}");
        response.IsSuccessStatusCode.Should().BeTrue();
    }
}
```

---

## §8 CI/CD Integration

### GitHub Actions
```yaml
name: .NET Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 8.0.x

      - run: dotnet restore

      - run: dotnet build --no-restore

      - name: Run Tests
        run: |
          dotnet test --no-build \
            --settings .runsettings \
            --logger "trx;LogFileName=results.trx" \
            --collect:"XPlat Code Coverage" \
            --results-directory ./TestResults

      - name: Generate Coverage Report
        run: |
          dotnet tool install -g dotnet-reportgenerator-globaltool
          reportgenerator \
            -reports:"TestResults/**/coverage.cobertura.xml" \
            -targetdir:"coveragereport" \
            -reporttypes:"Html;TextSummary"

      - name: Check Coverage
        run: |
          COVERAGE=$(grep 'Line coverage' coveragereport/Summary.txt | grep -oP '[\d.]+')
          echo "Coverage: $COVERAGE%"
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "Coverage below threshold!" && exit 1
          fi

      - name: Publish Results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: MSTest Results
          path: TestResults/*.trx
          reporter: dotnet-trx

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: |
            TestResults/
            coveragereport/
```

### Filtering Tests
```bash
# Run by category
dotnet test --filter "TestCategory=Integration"

# Run by name pattern
dotnet test --filter "FullyQualifiedName~UserService"

# Run by priority
dotnet test --filter "TestProperty.Priority=High"

# Exclude slow tests
dotnet test --filter "TestCategory!=Slow"
```

---

## §9 Debugging Table

| # | Problem | Cause | Fix |
|---|---------|-------|-----|
| 1 | Tests not discovered | Missing `[TestClass]` or `[TestMethod]` | Add both attributes; ensure project references MSTest.TestAdapter |
| 2 | `TestInitialize` runs every test | Expected behavior — per-test setup | Use `[ClassInitialize]` for one-time expensive setup |
| 3 | Parallel tests interfere | Shared static state | Use `[DoNotParallelize]` on conflicting classes or remove shared state |
| 4 | `DynamicData` not found | Property not `public static` | Ensure `IEnumerable<object[]>` property is `public static` |
| 5 | `ExpectedException` passes unexpectedly | Wrong exception type caught | Use `Assert.ThrowsExceptionAsync<T>` instead for precise control |
| 6 | `ClassInitialize` not called | Missing `TestContext` parameter | Signature must be `public static void/Task Method(TestContext context)` |
| 7 | FluentAssertions scope reports one | Not using `AssertionScope` | Wrap multiple assertions in `using (new AssertionScope())` |
| 8 | Async test hangs | Missing `await` or wrong return type | Return `Task` from async tests; use `await` on all async calls |
| 9 | Coverage reports 0% | coverlet not collecting | Add `--collect:"XPlat Code Coverage"` flag; check .runsettings |
| 10 | WebApplicationFactory startup fails | DI conflict with test services | Remove real DB descriptor before adding in-memory; check service order |
| 11 | `DataRow` doesn't accept complex types | DataRow only supports primitives | Use `[DynamicData]` with property/method for complex objects |
| 12 | `Timeout` attribute ignored | Applied to wrong scope | `[Timeout]` applies per-test; for class-wide use `.runsettings` |

---

## §10 Best Practices Checklist

1. Use `Assert.ThrowsExceptionAsync<T>` over `[ExpectedException]` — more precise, inspectable
2. Use `[DataTestMethod]` + `[DataRow]` for simple params, `[DynamicData]` for complex
3. Use `[TestInitialize]`/`[TestCleanup]` per test, `[ClassInitialize]`/`[ClassCleanup]` per class
4. Use FluentAssertions with `AssertionScope` — see all failures at once
5. Use Bogus for realistic test data — avoid brittle hard-coded values
6. Use `[TestCategory]` for CI filtering — separate unit from integration tests
7. Set `Parallelize` in `.runsettings` — scope to ClassLevel for safety
8. Use Testcontainers for real DB tests — match production behavior
9. Use W
...<truncated>
````
