Test Trait Tagging
Analyze an existing test suite and apply a standardized set of trait tags to each test method, giving teams visibility into their test distribution (positive vs. negative, critical-path coverage, smoke tests, etc.).
When to Use
- Auditing a test project to understand the mix of test types
- Adding trait attributes to untagged tests
- Generating a summary report of trait distribution across a test suite
- Reviewing whether critical paths have sufficient coverage
When Not to Use
- Writing new tests from scratch (use
writing-mstest-tests)
- Running or filtering tests (use
run-tests)
- Migrating between test frameworks
Inputs
| Input |
Required |
Description |
| Test project or files |
Yes |
Path to the test project, folder, or specific test files to analyze |
| Scope |
No |
tag (apply attributes), audit (report only), or both (default: both) |
| Framework |
No |
Auto-detected. Override with mstest, xunit, or nunit if detection fails |
Trait Taxonomy
Use exactly these trait names and values. Do not invent new trait values outside this table.
| Trait Value |
Meaning |
Heuristics |
positive |
Verifies expected behavior under normal/valid conditions |
Asserts success, valid output, expected state, no exceptions for valid input |
negative |
Verifies correct handling of invalid input, errors, or edge cases |
Asserts exceptions, error codes, validation failures, rejects bad input |
boundary |
Tests limits, thresholds, empty/null inputs, min/max values |
Operates on 0, -1, int.MaxValue, empty string, null, empty collection, boundary of valid range |
critical-path |
Core workflow that must never break; breakage blocks users |
Tests the primary success scenario of a key public API or user-facing feature |
smoke |
Quick sanity check that the system is operational |
Fast, no complex setup, verifies basic wiring (e.g., service resolves, endpoint returns 200) |
regression |
Reproduces a specific previously-reported bug |
References a bug ID, issue number, or describes a fix in its name or comments |
integration |
Crosses process, network, or persistence boundaries |
Uses real database, HTTP client, file system, external service, or multi-component setup |
end-to-end |
Full user workflow spanning the entire application stack |
Exercises a complete scenario from entry point to final result, distinct from single-boundary integration |
performance |
Validates timing, throughput, or resource consumption |
Asserts on elapsed time, memory, allocations, or uses benchmark harness |
security |
Verifies authentication, authorization, input sanitization, or secrets handling |
Tests for SQL injection, XSS, CSRF, unauthorized access, token validation, permission checks |
concurrency |
Validates thread safety, parallelism, or async correctness |
Uses Task.WhenAll, locks, Parallel.ForEach, SemaphoreSlim, reproduces race conditions |
resilience |
Tests retry logic, timeouts, circuit breakers, or graceful degradation |
Asserts behavior under transient failures, network drops, or service unavailability (e.g., Polly policies) |
destructive |
Mutates shared or external state that is hard to roll back |
Deletes records, drops resources, modifies global config — useful for CI isolation decisions |
configuration |
Verifies settings loading, defaults, environment behavior |
Tests missing config keys, invalid values, environment variable fallbacks, options validation |
flaky |
Known to intermittently fail (meta-tag for test health tracking) |
Mark tests the team knows are unreliable; used to quarantine or prioritize stabilization |
A single test may have multiple traits (e.g., both negative and boundary). At minimum, every test should receive one of positive or negative.
Workflow
Step 1: Detect the test framework
Examine project files (.csproj / .fsproj) and using directives to determine the framework:
| Signal |
Framework |
<PackageReference Include="MSTest..."> or using Microsoft.VisualStudio.TestTools.UnitTesting |
MSTest |
<PackageReference Include="xunit..."> or using Xunit |
xUnit |
<PackageReference Include="NUnit..."> or using NUnit.Framework |
NUnit |
Step 2: Scan existing traits
Check which tests already have trait attributes:
| Framework |
Existing Attribute |
Example |
| MSTest |
[TestCategory("...")] |
[TestCategory("positive")] |
| xUnit |
[Trait("Category", "...")] |
[Trait("Category", "positive")] |
| NUnit |
[Category("...")] |
[Category("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, Negative suggest negative
- Assertion type —
Assert.ThrowsException, Assert.Throws, Should().Throw() suggest negative
- Input values —
null, "", 0, -1, int.MaxValue, int.MinValue, empty collections suggest boundary
- Setup complexity — minimal setup with basic assertions suggests
smoke; external dependencies suggest integration
- Comments and names — references to issue numbers or "regression" / "bug" / "fix for #..." suggest
regression
- Timing assertions —
Stopwatch, BenchmarkDotNet, elapsed-time checks suggest performance
- 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 —
Task.WhenAll, Parallel.ForEach, locks, SemaphoreSlim, ConcurrentDictionary, race condition names 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
[Ignore]/[Skip] 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
Add the appropriate attribute to each test method. Place trait attributes on the line directly above or below the existing test attribute.
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
{ ... }
Step 5: Generate trait summary
After tagging, produce a summary table:
## Trait Distribution
| Trait | Count | % of Total |
|---------------|-------|------------|
| positive | 42 | 53.8% |
| negative | 22 | 28.2% |
| boundary | 8 | 10.3% |
| critical-path | 12 | 15.4% |
| smoke | 3 | 3.8% |
| regression | 5 | 6.4% |
| integration | 4 | 5.1% |
| end-to-end | 2 | 2.6% |
| performance | 1 | 1.3% |
| security | 3 | 3.8% |
| concurrency | 2 | 2.6% |
| resilience | 1 | 1.3% |
| destructive | 1 | 1.3% |
| configuration | 2 | 2.6% |
| flaky | 1 | 1.3% |
| **Total tests** | **78** | — |
Note: Percentages exceed 100% because tests can have multiple traits.
Include observations such as:
- Ratio of positive to negative tests
- Whether critical-path tests exist for key public APIs
- Any tests that could not be confidently classified (list them for manual review)
Validation
Common Pitfalls
| Pitfall |
Solution |
| Guessing traits without reading the test body |
Always read assertions and setup to classify accurately |
Tagging a test only as boundary without positive/negative |
Every test should also be positive or negative — boundary is additive |
Using TestCategory syntax in an xUnit project |
Match the attribute style to the detected framework |
| Duplicating an existing category attribute |
Check for pre-existing traits in Step 2 before adding |
Over-tagging as critical-path |
Reserve for tests on primary public entry points, not every helper |
1---2name: exp-test-tagging3description: Analyzes test suites and tags each test with a standardized set of traits (e.g., positive, negative, critical-path, boundary, smoke, regression). Use when the user wants to categorize, audit, or label tests with traits. Do not use for writing new tests, running tests, or migrating test frameworks.4---5
6# Test Trait Tagging
7
8Analyze an existing test suite and apply a standardized set of trait tags to each test method, giving teams visibility into their test distribution (positive vs. negative, critical-path coverage, smoke tests, etc.).
9
10## When to Use
11
12- Auditing a test project to understand the mix of test types
13- Adding trait attributes to untagged tests
14- Generating a summary report of trait distribution across a test suite
15- Reviewing whether critical paths have sufficient coverage
16
17## When Not to Use
18
19- Writing new tests from scratch (use `writing-mstest-tests`)
20- Running or filtering tests (use `run-tests`)
21- Migrating between test frameworks
22
23## Inputs
24
25| Input | Required | Description |
26|-------|----------|-------------|
27| Test project or files | Yes | Path to the test project, folder, or specific test files to analyze |
28| Scope | No | `tag` (apply attributes), `audit` (report only), or `both` (default: `both`) |
29| Framework | No | Auto-detected. Override with `mstest`, `xunit`, or `nunit` if detection fails |
30
31## Trait Taxonomy
32
33Use exactly these trait names and values. Do not invent new trait values outside this table.
34
35| Trait Value | Meaning | Heuristics |
36|-------------|---------|------------|
37| `positive` | Verifies expected behavior under normal/valid conditions | Asserts success, valid output, expected state, no exceptions for valid input |
38| `negative` | Verifies correct handling of invalid input, errors, or edge cases | Asserts exceptions, error codes, validation failures, rejects bad input |
39| `boundary` | Tests limits, thresholds, empty/null inputs, min/max values | Operates on `0`, `-1`, `int.MaxValue`, empty string, null, empty collection, boundary of valid range |
40| `critical-path` | Core workflow that must never break; breakage blocks users | Tests the primary success scenario of a key public API or user-facing feature |
41| `smoke` | Quick sanity check that the system is operational | Fast, no complex setup, verifies basic wiring (e.g., service resolves, endpoint returns 200) |
42| `regression` | Reproduces a specific previously-reported bug | References a bug ID, issue number, or describes a fix in its name or comments |
43| `integration` | Crosses process, network, or persistence boundaries | Uses real database, HTTP client, file system, external service, or multi-component setup |
44| `end-to-end` | Full user workflow spanning the entire application stack | Exercises a complete scenario from entry point to final result, distinct from single-boundary `integration` |
45| `performance` | Validates timing, throughput, or resource consumption | Asserts on elapsed time, memory, allocations, or uses benchmark harness |
46| `security` | Verifies authentication, authorization, input sanitization, or secrets handling | Tests for SQL injection, XSS, CSRF, unauthorized access, token validation, permission checks |
47| `concurrency` | Validates thread safety, parallelism, or async correctness | Uses `Task.WhenAll`, locks, `Parallel.ForEach`, `SemaphoreSlim`, reproduces race conditions |
48| `resilience` | Tests retry logic, timeouts, circuit breakers, or graceful degradation | Asserts behavior under transient failures, network drops, or service unavailability (e.g., Polly policies) |
49| `destructive` | Mutates shared or external state that is hard to roll back | Deletes records, drops resources, modifies global config — useful for CI isolation decisions |
50| `configuration` | Verifies settings loading, defaults, environment behavior | Tests missing config keys, invalid values, environment variable fallbacks, options validation |
51| `flaky` | Known to intermittently fail (meta-tag for test health tracking) | Mark tests the team knows are unreliable; used to quarantine or prioritize stabilization |
52
53A single test may have **multiple traits** (e.g., both `negative` and `boundary`). At minimum, every test should receive one of `positive` or `negative`.
54
55## Workflow
56
57### Step 1: Detect the test framework
58
59Examine project files (`.csproj` / `.fsproj`) and `using` directives to determine the framework:
60
61| Signal | Framework |
62|--------|-----------|
63| `<PackageReference Include="MSTest...">` or `using Microsoft.VisualStudio.TestTools.UnitTesting` | MSTest |
64| `<PackageReference Include="xunit...">` or `using Xunit` | xUnit |
65| `<PackageReference Include="NUnit...">` or `using NUnit.Framework` | NUnit |
66
67### Step 2: Scan existing traits
68
69Check which tests already have trait attributes:
70
71| Framework | Existing Attribute | Example |
72|-----------|--------------------|---------|
73| MSTest | `[TestCategory("...")]` | `[TestCategory("positive")]` |
74| xUnit | `[Trait("Category", "...")]` | `[Trait("Category", "positive")]` |
75| NUnit | `[Category("...")]` | `[Category("positive")]` |
76
77Record which tests already have tags to avoid duplication.
78
79### Step 3: Classify each test method
80
81For each test method without traits, analyze:
82
831. **Method name** — names containing `Invalid`, `Fail`, `Error`, `Throw`, `Reject`, `BadInput`, `Null`, `Negative` suggest `negative`
842. **Assertion type** — `Assert.ThrowsException`, `Assert.Throws`, `Should().Throw()` suggest `negative`
853. **Input values** — `null`, `""`, `0`, `-1`, `int.MaxValue`, `int.MinValue`, empty collections suggest `boundary`
864. **Setup complexity** — minimal setup with basic assertions suggests `smoke`; external dependencies suggest `integration`
875. **Comments and names** — references to issue numbers or "regression" / "bug" / "fix for #..." suggest `regression`
886. **Timing assertions** — `Stopwatch`, `BenchmarkDotNet`, elapsed-time checks suggest `performance`
897. **Feature centrality** — tests on primary public API entry points or critical user workflows suggest `critical-path`
908. **Security patterns** — validates auth, checks permissions, sanitizes input, tests for injection, handles tokens/secrets suggest `security`
919. **Parallel/async constructs** — `Task.WhenAll`, `Parallel.ForEach`, locks, `SemaphoreSlim`, `ConcurrentDictionary`, race condition names suggest `concurrency`
9210. **Fault injection** — simulates failures, tests retries, timeouts, or circuit breakers suggest `resilience`
9311. **State mutation** — deletes external records, drops resources, modifies shared/global state suggest `destructive`
9412. **Full-stack flow** — test spans entry point through data layer to final response, covering a complete user scenario suggest `end-to-end`
9513. **Config/settings** — loads configuration, tests missing keys, validates options, checks environment variables suggest `configuration`
9614. **Known instability** — test has `[Ignore]`/`[Skip]` comments about flakiness, or names contain "flaky"/"intermittent" suggest `flaky`
9715. **Default** — if the test verifies a normal success path, tag `positive`
98
99When in doubt between `positive` and `negative`, read the assertion: if it asserts success → `positive`; if it asserts failure → `negative`.
100
101### Step 4: Apply trait attributes
102
103Add the appropriate attribute to each test method. Place trait attributes on the line directly above or below the existing test attribute.
104
105**MSTest:**
106```csharp
107[TestMethod]
108[TestCategory("negative")]
109[TestCategory("boundary")]
110public void Parse_NullInput_ThrowsArgumentNullException() { ... }
111```
112
113**xUnit:**
114```csharp
115[Fact]
116[Trait("Category", "positive")]
117[Trait("Category", "critical-path")]
118public void CreateOrder_ValidItems_ReturnsConfirmation() { ... }
119```
120
121**NUnit:**
122```csharp
123[Test]
124[Category("regression")]
125[Category("negative")]
126public void Calculate_OverflowInput_ReturnsError() // Fix for #1234
127{ ... }
128```
129
130### Step 5: Generate trait summary
131
132After tagging, produce a summary table:
133
134```
135## Trait Distribution
136
137| Trait | Count | % of Total |
138|---------------|-------|------------|
139| positive | 42 | 53.8% |
140| negative | 22 | 28.2% |
141| boundary | 8 | 10.3% |
142| critical-path | 12 | 15.4% |
143| smoke | 3 | 3.8% |
144| regression | 5 | 6.4% |
145| integration | 4 | 5.1% |
146| end-to-end | 2 | 2.6% |
147| performance | 1 | 1.3% |
148| security | 3 | 3.8% |
149| concurrency | 2 | 2.6% |
150| resilience | 1 | 1.3% |
151| destructive | 1 | 1.3% |
152| configuration | 2 | 2.6% |
153| flaky | 1 | 1.3% |
154| **Total tests** | **78** | — |
155
156Note: Percentages exceed 100% because tests can have multiple traits.
157```
158
159Include observations such as:
160- Ratio of positive to negative tests
161- Whether critical-path tests exist for key public APIs
162- Any tests that could not be confidently classified (list them for manual review)
163
164## Validation
165
166- [ ] Every test method has at least one trait attribute (`positive` or `negative` at minimum)
167- [ ] No invented trait values outside the taxonomy table
168- [ ] Existing trait attributes were preserved, not duplicated
169- [ ] The trait summary table was generated
170- [ ] The project still builds after changes (`dotnet build`)
171
172## Common Pitfalls
173
174| Pitfall | Solution |
175|---------|----------|
176| Guessing traits without reading the test body | Always read assertions and setup to classify accurately |
177| Tagging a test only as `boundary` without `positive`/`negative` | Every test should also be `positive` or `negative` — `boundary` is additive |
178| Using `TestCategory` syntax in an xUnit project | Match the attribute style to the detected framework |
179| Duplicating an existing category attribute | Check for pre-existing traits in Step 2 before adding |
180| Over-tagging as `critical-path` | Reserve for tests on primary public entry points, not every helper |