GitHub Copilot Testing in Visual Studio 2026: Generate xUnit Tests for C#
Turn AI-assisted test generation into a reviewable workflow with explicit behavior, meaningful xUnit assertions, and reproducible execution.
Start with seven passing discount-calculator cases, add three missing examples, and check whether the assertions actually notice a broken calculation. The result is a small xUnit suite you can run without an AI account, plus a concrete prompt and review workflow for Visual Studio's dedicated Copilot testing feature.
The shared source archive contains the real calculator and tests used here. The test additions and controlled defect experiments were implemented and executed for this revision; they are not presented as Copilot-generated output. No Visual Studio/Copilot session was run. The IDE steps below are based on Microsoft documentation, while the command-line results come from actual local execution.
Check the feature and prerequisites
The two paths have different requirements. Running the sample tests needs the .NET 10 SDK and package restore access, with no Copilot account or provider key. The archive's separate browser-parser tests additionally need Node.js. Using the dedicated IDE feature requires Visual Studio 2026 version 18.3 or later, C# code and a GitHub account signed into Visual Studio with a paid Copilot subscription, according to the Microsoft Copilot testing documentation, rechecked November 2, 2026.
This is the Visual Studio feature entered through @Test, not a Visual Studio Code command or a claim about every Copilot Chat interface. Microsoft also documents the editor route Copilot Actions > Generate Tests. Missing IDE access does not prevent you from following the calculator exercise through the terminal.
Extract the ZIP and work in its Jovenzo.Samples directory. Keep Core, Api, Sdk and Tests together; do not create another solution for this exercise. These three files are the relevant starting points:
| Archive-relative file | What to inspect |
|---|---|
src/Jovenzo.Samples.Core/DiscountCalculator.cs | The production operation and its input/rounding contract. |
tests/Jovenzo.Samples.Tests/DiscountCalculatorTests.cs | Decimal examples and exact result/exception assertions. |
tests/Jovenzo.Samples.Tests/Jovenzo.Samples.Tests.csproj | xUnit 2.9.3, VS runner 3.1.4 and Microsoft.NET.Test.Sdk 17.14.1. |
The official xUnit v2 guide explains the framework and VSTest setup used here. The runner's 3.1.4 version does not make this an xUnit v3 test project. Preserve the existing packages and lock files rather than replacing the project with a new template. The separate xUnit v3 guide is supplemental reading for a different framework generation, not this sample's setup instructions.
Write the behavior before requesting tests
The calculator accepts a nonnegative decimal price and a discount percentage from zero through one hundred, inclusive. It applies the discount, then rounds the final price to two decimal places with MidpointRounding.AwayFromZero. Invalid inputs throw ArgumentOutOfRangeException naming the rejected public parameter. The existing implementation already satisfies the cases below and is unchanged in this revision.
public static class DiscountCalculator
{
public static decimal FinalPrice(decimal price, decimal discountPercent)
{
if (price < 0) throw new ArgumentOutOfRangeException(nameof(price));
if (discountPercent is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(discountPercent));
return decimal.Round(price * (1 - discountPercent / 100m), 2, MidpointRounding.AwayFromZero);
}
}
The half-cent case makes the rounding choice visible. A price of 0.05 with a 10% discount removes 0.005, leaving 0.045 before rounding. Two decimal places require choosing between 0.04 and 0.05. AwayFromZero chooses 0.05 for this positive midpoint; ToEven chooses 0.04 because its last retained digit is even. A test expecting 0.05 therefore distinguishes two plausible implementations.
Expected values are explicit examples of the contract. A full discount on 100 produces zero; a 12.5% discount on 80 removes 10 and leaves 70. The tests store these outcomes directly instead of recalculating them with the production formula. Repeating that formula inside an assertion would allow the same wrong divisor to appear on both sides.
This is a teaching operation, not a complete pricing engine. It defines no tax ordering, currency conversion, coupon eligibility or persistence. Those concerns would need separate requirements and tests. Adding a mocking package or a pricing architecture would make this exercise harder to follow without improving evidence about its actual rules.
Give Copilot a narrow, reviewable request
First build and run the existing suite using the commands in the execution section below. The recorded pre-change calculator baseline was seven passed, none failed or skipped. A reader downloading this updated revision starts with ten cases already present; that reader should record the current result before requesting further changes rather than expecting to recreate the old baseline.
- Open
Jovenzo.Samples.slnxin the eligible Visual Studio installation and open DiscountCalculator.cs. - Choose
View > GitHub Copilot Chatand target the class with the dedicated@Testentry point. - Provide the behavior rules and existing test-project location in the request below.
- Inspect every proposed code, project and package change before keeping it.
- Run the focused tests, then the full suite; use Test Explorer to inspect failures and the terminal commands to reproduce them.
@Test Generate or extend xUnit tests for DiscountCalculator.
Use tests/Jovenzo.Samples.Tests and its existing xUnit 2.9.3 conventions.
Price must be nonnegative; discountPercent is between 0 and 100 inclusive.
Round the final price to two places with MidpointRounding.AwayFromZero.
Cover zero price, fractional percentage, no-discount rounding,
the half-cent example, range boundaries and invalid parameter names.
Use explicit expected values; do not copy the production formula.
Do not change production code or add packages.
Explain any proposed project changes before proceeding.
Explain which behavior each new case protects.
This is an illustrative request, not a transcript. “Use the existing project” expresses the desired scope; it does not guarantee how the tool will organize its edits. Microsoft's documented workflow can create a separate test project. Check the solution and project diff, even if the final tests pass. An added runner, mocking package or framework migration requires its own reason and should not enter unnoticed.
The Microsoft feature announcement describes generation followed by build, run and attempted repairs. A repair still needs review: correcting a namespace is different from deleting a failing boundary assertion or changing the rounding rule. Return to the stated contract when the tool proposes making a failure disappear.
Review the xUnit cases as specifications
The final test file has seven valid-price rows and three invalid-input rows. The original four valid examples remain. Three additions protect distinct gaps: zero is a valid price, percentages can be fractional, and even a zero discount must round the final price. The existing rejection cases now check ParamName as well as the exact exception type.
using Jovenzo.Samples;
using Xunit;
namespace Jovenzo.Samples.Tests;
public sealed class DiscountCalculatorTests
{
public static TheoryData<decimal, decimal, decimal> ValidPrices => new()
{
{ 100m, 0m, 100m },
{ 100m, 100m, 0m },
{ 19.99m, 15m, 16.99m },
{ 0.05m, 10m, 0.05m },
{ 0m, 45m, 0m }, // Zero is an allowed price.
{ 80m, 12.5m, 70m }, // Fractional percentages must not be truncated.
{ 19.995m, 0m, 20m } // No discount still rounds the final price.
};
[Theory]
[MemberData(nameof(ValidPrices))]
public void AppliesDiscountAndRoundsAwayFromZero(decimal price, decimal percentage, decimal expected) =>
Assert.Equal(expected, DiscountCalculator.FinalPrice(price, percentage));
public static TheoryData<decimal, decimal, string> InvalidArguments => new()
{
{ -1m, 0m, "price" },
{ 1m, -1m, "discountPercent" },
{ 1m, 101m, "discountPercent" }
};
[Theory]
[MemberData(nameof(InvalidArguments))]
public void RejectsOutOfRangeArguments(decimal price, decimal percentage, string expectedParameter)
{
var error = Assert.Throws<ArgumentOutOfRangeException>(() => DiscountCalculator.FinalPrice(price, percentage));
Assert.Equal(expectedParameter, error.ParamName);
}
}
Typed TheoryData supplies real decimal values through MemberData. The m suffix belongs in ordinary C# data initialization; decimal literals cannot be passed directly as attribute arguments. This avoids relying on conversion from a double-valued InlineData attribute and keeps the 12.5 and 19.995 examples visibly decimal throughout the test setup.
| Added or strengthened case | Explicit expectation | Behavior protected |
|---|---|---|
| 0 price, 45% discount | 0 | A zero price is accepted, not rejected as negative or invalid. |
| 80 price, 12.5% discount | 70 | A fractional percentage participates in the calculation without integer truncation. |
| 19.995 price, 0% discount | 20.00 | No-discount execution still applies final two-place rounding. |
| Negative price | ArgumentOutOfRangeException, ParamName price | The public error identifies the invalid price argument. |
| Percentage -1 or 101 | ArgumentOutOfRangeException, ParamName discountPercent | Both out-of-range directions identify the percentage argument. |
The last three rejection rows were strengthened, not duplicated just to increase the count. Their expected parameter name is the calculator's public discountPercent, even though the test method calls its local input percentage. That difference matters: an exception naming the test's variable would not satisfy the API contract. Decimal equality checks numeric value, so an expected 20m also represents the 20.00 result without asserting presentation formatting.
Run the tests and diagnose the right failure
From the extracted Jovenzo.Samples directory, restore the locked dependencies, build all four projects in Release and run the complete C# suite. The focused command uses --no-build and --no-restore, so the Release build must already have completed after the latest edits. Omitting that rebuild can test yesterday's binary instead of today's source.
dotnet restore Jovenzo.Samples.slnx --locked-mode
dotnet build Jovenzo.Samples.slnx -c Release --no-restore --disable-build-servers -m:1
dotnet test tests/Jovenzo.Samples.Tests/Jovenzo.Samples.Tests.csproj -c Release --no-build --no-restore
# Calculator-only selection, after the Release build above:
dotnet test tests/Jovenzo.Samples.Tests/Jovenzo.Samples.Tests.csproj -c Release --no-build --no-restore --filter "FullyQualifiedName~Jovenzo.Samples.Tests.DiscountCalculatorTests"
# Separate browser-parser regression suite; requires Node.js:
node --test tests/stream-browser.test.mjs
The ten calculator cases are a subset of the shared total, not an additional ten tests to add to it. The JavaScript suite belongs to the archive's SSE browser client and is unrelated to the calculator's arithmetic. It is run to check that the common download remains usable. None of these commands invokes a live AI model or needs a Copilot subscription.
Read the failure category before changing code. Compiler or discovery errors mean the assertions have not established anything yet. A discovered test with an expected/actual mismatch is behavioral evidence to compare with the contract. A timeout or unavailable SDK is a different problem again. Test Explorer is useful for locating a row, but a repeatable command and its recorded result make the evidence reviewable outside the IDE.
Find tests that pass for the wrong reason
For the same 0.05/10% input, a broad range assertion would accept both the required 0.05 and the incorrect ToEven result 0.04. The following weak assertion is deliberately constructed for explanation; it is not claimed to be Copilot output or part of the shipped suite. The exact equality is the useful behavior check, expressed through the theory's expected value in the real file.
var actual = DiscountCalculator.FinalPrice(0.05m, 10m);
// Weak illustrative assertion: accepts both 0.04 and 0.05.
Assert.InRange(actual, 0m, 0.05m);
// Strong assertion for the specified midpoint rule.
Assert.Equal(0.05m, actual);
To check the actual assertions, three small defects were introduced one at a time in a separate temporary source copy. Each version was rebuilt before the focused tests ran. A fresh successful build and ten discovered test cases were required for every result below; compilation or discovery failures were not counted as a detected defect.
| Temporary change | Expected | Observed after rebuild | Catching test |
|---|---|---|---|
| AwayFromZero → ToEven | Midpoint row should fail | 1 failed, 9 passed; expected 0.05, actual 0.04 | AppliesDiscountAndRoundsAwayFromZero: price 0.05, percentage 10 |
| Divisor 100m → 10m | Discount results should differ | 4 failed, 6 passed; full-discount row expected 0, actual -900 | AppliesDiscountAndRoundsAwayFromZero: 100/100, 19.99/15, 0.05/10 and 80/12.5 rows |
| Remove the negative-price guard | Rejection row should fail | 1 failed, 9 passed; Assert.Throws found no exception | RejectsOutOfRangeArguments: price -1, percentage 0 |
After restoring the original calculator bytes, the temporary copy was rebuilt and all ten calculator tests passed again. The main production file was never edited for these experiments. Broken variants, experiment logs and build output are excluded from the ZIP. These are three controlled mutation experiments, not an automated mutation-testing tool run, comprehensive defect search or mutation-score measurement.
The divisor experiment shows why a passing test count alone is unhelpful. Six rows still pass because some inputs do not distinguish the wrong calculation: zero price stays zero and zero discount still leaves the rounded price. The four failing rows supply the missing discrimination. The range experiment checks exception occurrence; the ordinary clean run also verifies its parameter name. No single assertion proves every property of the method.
Extend the workflow to asynchronous backend code
The calculator needs no substitute dependencies. For a later HTTP operation, the same review pattern can use a fake HttpMessageHandler to control success, malformed input, status failures and cancellation. Await the operation and use asynchronous exception assertions; coordinate cancellation after the fake reaches a known state instead of guessing with a short sleep. The OpenAI integration guide applies that transport boundary in the same archive. This is an extension of the method, not a reason to add HTTP or mocking infrastructure to the calculator.
Use coverage as a map, then review the evidence
No coverage measurement was performed for this revision. A coverage percentage would describe executed code under a particular run, not whether every expectation is useful. Here, the midpoint experiment provides more specific evidence: a particular changed rounding rule caused a particular equality assertion to fail. It still says nothing about untried defects or a complete financial domain.
Review proposed generated changes against the same concrete evidence. Keep assertions with an identifiable requirement, inspect new project/package files, and reject repairs that weaken a boundary merely to regain green status. Repository conventions can record the calculator location, the focused command and the pinned framework; the AGENTS.md guide explains how to make those instructions reusable. Instructions help communicate intent, but the resulting diff still needs inspection.
Use synthetic values like the small prices here when asking for tests. Check account and organization policies before sending proprietary code or customer data through an IDE assistant. No private fixture is necessary to explain this rounding rule. For broader backend work, a unit result also leaves deployment questions open: parser tests do not establish proxy streaming behavior, as the SSE tutorial discusses.
Verification results — November 2, 2026
Package revision calculator-tests-1, based on cfe7772 plus this revision's documented test changes: the original seven calculator cases passed before editing. The expanded calculator subset passed 10 tests. All four shared projects built in Release with zero warnings or errors; the complete C# suite passed 226 tests, 0 failed, 0 skipped, including those ten calculator cases. The unchanged JavaScript suite was freshly run: 45 passed.
The refreshed ZIP was extracted to a separate directory: locked restore and all four Release builds succeeded with zero warnings or errors; the extracted tests passed 226 C# cases, the selected 10 calculator cases and 45 JavaScript cases. The archive contains 45 source/documentation/test files; SHA-256: d6bf84df010be3f62e087ff297c6f3a8123062064b7fde8e3bbac7bcf7c65ac6. The three separately rebuilt defect experiments produced assertion failures as shown above; the restored copy passed 10/10. No automated mutation tool, coverage measurement or actual Visual Studio/Copilot generation session was run. The test edits were made during repository maintenance, not attributed to Copilot. No live AI request, IDE screenshot, productivity improvement or tool-success rate is claimed.
Key takeaways
- Run the existing tests before prompting; keep the baseline separate from generated changes.
- The shared ZIP already has the calculator, xUnit v2 project and reproducible commands.
- Explicit decimal outcomes and parameter names protect concrete contract details.
- Three rebuilt defect experiments show selected assertions catching selected errors, not comprehensive correctness.
Frequently asked questions
Do I need Copilot to run these tests?
No. The terminal tests need .NET and dependency restore; the dedicated Visual Studio @Test workflow has separate account and IDE requirements.
Did Copilot generate the tests shown here?
No such claim is made. The repository tests were edited and executed directly; the Copilot prompt is a documented workflow example that has not been exercised here.
Does catching all three changes establish a mutation score?
No. Only three deliberately chosen source changes were tried. A comprehensive mutation analysis would require its own tooling, scope and evidence.
Conclusion and next step
You now have ten executable calculator cases, explicit reasons for the three added inputs and stronger exception checks, and recorded evidence that the suite detects three selected defects. Use that baseline when trying Visual Studio @Test: state the contract, review the proposed diff and rerun the same commands. Keep the IDE result separate from what the command-line suite actually proves.
