Use the open-source free `coverlet` toolchain for .NET code coverage.
Test Trait Tagging
Analyzes test suites in any language and tags each test with standardized traits (positive, negative, critical-path, boundary, smoke, regression, integration, performance, security). Use when the user wants to categorize, audit, or label tests with traits. Works across .NET (MSTest/xUnit/NUnit/TUnit), Python (pytest), TS/JS (Jest/Vitest), Java, Go, Ruby, Rust, Swift, Kotlin, PowerShell, and C++ — auto-editing when the framework has canonical tag syntax, otherwise report-only. Do not use for writing new tests, running tests, or migrating frameworks.
Workflow
Step 1: Detect the language, framework, and tagging capability
Identify the codebase's language and test framework. Call the test-analysis-extensions skill and read the matching extension file. The extension file declares a tag-support capability for each framework:
- `auto-edit` — framework has canonical tag syntax this skill can safely insert (.NET
[TestCategory]/[Trait]/[Category]/[Property], pytest@pytest.mark.<name>, JUnit 5@Tag("..."), TestNGgroups = {"..."}, RSpec metadatait "..." , :tag => true, Pester-Tag '...', Kotest@Tags(...), Swift Testing@Tag(.tagName), Catch2[tag], doctest* doctest::test_suite("tag")decorator). - `report-only` — framework has no canonical, agreed-upon tag attribute; report tags in a Markdown table only and do not edit source (Go standard
testingwithout build-tag conventions, Jest/Vitest without consistent describe-prefix convention, Rust without project-specific cfg conventions, XCTest without a test plan, GoogleTest without test-name prefix conventions, Mocha without describe-prefix conventions). - `convention-based` — framework uses naming or file conventions for tagging (Go
//go:build integrationbuild tags, file-name suffixes like*_integration_test.go, GoogleTestINTEGRATION_*filter prefix). Only emit canonical edits when the user has confirmed the project convention; otherwise treat asreport-only.
Capture the capability before Step 4.
Step 2: Scan existing traits
Check which tests already have trait attributes. Use the loaded language extension as the source of truth — examples:
| Framework | Existing Attribute | Example | |-----------|--------------------|---------| | MSTest | [TestCategory("...")] | [TestCategory("positive")] | | xUnit | [Trait("Category", "...")] | [Trait("Category", "positive")] | | NUnit | [Category("...")] | [Category("positive")] | | TUnit | [Property("Category", "...")] | [Property("Category", "positive")] | | JUnit 5 | @Tag("...") | @Tag("positive") | | TestNG | @Test(groups = {"..."}) | @Test(groups = {"positive"}) | | pytest | @pytest.mark.<name> | @pytest.mark.positive | | RSpec | metadata after it | it "...", :positive do | | Pester | -Tag '...' | It '...' -Tag 'positive' | | Kotest | @Tags(...) | @Tags(Positive) | | Swift Testing | @Tag(.<name>) | @Test(.tags(.positive)) | | Catch2 | [tag] in name | TEST_CASE("...", "[positive]") | | doctest | * doctest::test_suite("...") decorator | TEST_CASE("..." *doctest::test_suite("positive")) |
Record which tests already have tags to avoid duplication.
Step 3: Classify each test method
For each test method without traits, analyze:
- Method name -- names containing
Invalid,Fail,Error,Throw,Reject,BadInput,Null,None,Nil,Negative,raises_,_throws_,_returns_errorsuggestnegative - Assertion type --
Assert.ThrowsException/Assert.Throws/Should().Throw()/pytest.raises/expect(fn).toThrow/assertThrows/assert.Error(t, err)/expect { ... }.to raise_error/#[should_panic]/XCTAssertThrowsError/Should -Throw/EXPECT_THROWsuggestnegative - Input values --
null/None/nil/undefined,"",0,-1,int.MaxValue/sys.maxsize/Number.MAX_SAFE_INTEGER/math.MaxInt64/i32::MAX, empty collections suggestboundary - Setup complexity -- minimal setup with basic assertions suggests
smoke; external dependencies (file/db/net/env) suggestintegration - Comments and names -- references to issue numbers or "regression" / "bug" / "fix for #..." suggest
regression - Timing assertions --
Stopwatch,BenchmarkDotNet, elapsed-time checks; pytest-benchmark fixtures; benchmark.js; JMH@Benchmark;go test -bench; criterion.rs; XCTMetric; Google Benchmark; kotlinx-benchmark suggestperformance - Feature centrality -- tests on primary public API entry points or critical user workflows suggest
critical-path - Security patterns -- validates auth, checks permissions, sanitizes input, tests for injection, handles tokens/secrets suggest
security - Parallel/async constructs -- per-language concurrency primitives (see Trait Taxonomy table) suggest
concurrency - Fault injection -- simulates failures, tests retries, timeouts, or circuit breakers suggest
resilience - State mutation -- deletes external records, drops resources, modifies shared/global state suggest
destructive - Full-stack flow -- test spans entry point through data layer to final response, covering a complete user scenario suggest
end-to-end - Config/settings -- loads configuration, tests missing keys, validates options, checks environment variables suggest
configuration - Known instability -- test has skip / ignore annotations with comments about flakiness, or names contain "flaky" / "intermittent" suggest
flaky - Default -- if the test verifies a normal success path, tag
positive
When in doubt between positive and negative, read the assertion: if it asserts success -> positive; if it asserts failure -> negative.
Step 4: Apply trait attributes (or report only)
If the loaded language extension declares `auto-edit` for the framework, add the appropriate attribute to each test method. Place trait attributes adjacent to the existing test attribute. Examples:
MSTest:
[TestMethod]
[TestCategory("negative")]
[TestCategory("boundary")]
public void Parse_NullInput_ThrowsArgumentNullException() { ... }
xUnit:
[Fact]
[Trait("Category", "positive")]
[Trait("Category", "critical-path")]
public void CreateOrder_ValidItems_ReturnsConfirmation() { ... }
NUnit:
[Test]
[Category("regression")]
[Category("negative")]
public void Calculate_OverflowInput_ReturnsError() // Fix for #1234
{ ... }
pytest:
@pytest.mark.negative
@pytest.mark.boundary
def test_parse_none_input_raises_value_error():
...
JUnit 5:
@Test
@Tag("positive")
@Tag("critical-path")
void createOrder_validItems_returnsConfirmation() { ... }
TestNG:
@Test(groups = {"negative", "boundary"})
public void parse_nullInput_throwsIllegalArgumentException() { ... }
RSpec:
it "rejects null input", :negative, :boundary do
...
end
Pester:
It 'Rejects null input' -Tag 'negative','boundary' {
...
}
Kotest:
@Tags(Negative, Boundary)
class ParserSpec : StringSpec({
"rejects null input" { ... }
})
Swift Testing:
@Test(.tags(.negative, .boundary))
func parseNullInputThrows() throws { ... }
Catch2:
TEST_CASE("Parse null input throws", "[negative][boundary]") { ... }
If the loaded language extension declares `report-only` for the framework (Go standard testing, plain Jest/Vitest without convention, Rust without project-specific cfg, plain XCTest, plain GoogleTest, plain Mocha), do NOT modify source files. Instead emit a Markdown table mapping each test to its suggested tags, and recommend a project-wide convention the team can adopt (build tags, file suffix, describe-block prefix, GoogleTest filter prefix, test-plan grouping, etc.).
If the loaded language extension declares `convention-based` (e.g., Go //go:build integration, *_integration_test.go, GoogleTest INTEGRATION_* prefix), only emit canonical edits when the user has confirmed the project's convention. Otherwise treat as report-only.
Step 5: Generate trait summary
After tagging, produce a summary table:
Related skills
Write, run, or repair .NET tests that use MSTest.
Write, run, or repair .NET tests that use NUnit.