Test-gating for .NET — skip tests dynamically via a Google Spreadsheet, without touching your test code.
Skipper reads a Google Spreadsheet to decide which tests to skip. Each row has:
| testId | disabledUntil | notes |
|---|---|---|
Tests/Unit/AuthTests.cs > AuthTests > CanLogin |
2026-06-01 |
Flaky on CI |
Tests/E2E/LoginTests.cs > LoginTests > UserCanLogin |
disabledUntilis in the future → test is skipped automaticallydisabledUntilis empty or in the past → test runs normally- Test not in spreadsheet → test runs normally (opt-out model)
No code changes required in your tests. Configure once, gate everywhere.
| Package | Description | NuGet |
|---|---|---|
GetSkipper.Core |
Google Sheets client, resolver, writer | |
GetSkipper.XUnit |
xUnit v2/v3 integration | |
GetSkipper.NUnit |
NUnit 3/4 integration | |
GetSkipper.MSTest |
MSTest v3 integration | |
GetSkipper.Playwright |
Playwright for .NET | |
GetSkipper.SpecFlow |
Reqnroll (SpecFlow) BDD |
// AssemblyInfo.cs — add once to your test project
using GetSkipper.XUnit;
[assembly: TestFramework("GetSkipper.XUnit.SkipperTestFramework", "GetSkipper.XUnit")]
[assembly: SkipperConfig(
SpreadsheetIdEnvVar = "SKIPPER_SPREADSHEET_ID",
CredentialsFile = "service-account.json")]
// Your tests remain unchanged:
public class AuthTests
{
[Fact]
public void CanLogin() { /* ... */ }
}// AssemblyInfo.cs
using GetSkipper.NUnit;
[assembly: SkipperConfig(
SpreadsheetId = "1abc...",
CredentialsFile = "service-account.json")]
// Your tests remain unchanged:
[TestFixture]
public class AuthTests
{
[Test]
public void CanLogin() { /* ... */ }
}// SkipperSetup.cs — add once to your test project
using GetSkipper.Core;
using GetSkipper.Core.Credentials;
using GetSkipper.MSTest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
public static class SkipperSetup
{
[GlobalTestInitialize]
public static async Task InitAsync()
=> await SkipperGlobalHooks.ConfigureAsync(new SkipperConfig
{
SpreadsheetId = "1abc...",
Credentials = new FileCredentials("service-account.json"),
});
[GlobalTestCleanup]
public static Task CleanupAsync() => SkipperGlobalHooks.AfterAllTestsAsync();
}
// Intercept each test (call from a second [GlobalTestInitialize] or use one method):
public static class SkipperTestGate
{
[GlobalTestInitialize]
public static void BeforeTest(TestContext ctx) => SkipperGlobalHooks.BeforeTest(ctx);
}using GetSkipper.NUnit;
using GetSkipper.Playwright;
[assembly: SkipperConfig(SpreadsheetId = "1abc...", CredentialsFile = "service-account.json")]
[TestFixture]
public class LoginTests : SkipperPageTest // was: PageTest
{
[Test]
public async Task UserCanLogin()
{
await Page.GotoAsync("/login");
// automatic skip is handled before this line
}
}// reqnroll.json
{
"bindingAssemblies": [
{ "assembly": "GetSkipper.SpecFlow" }
]
}SKIPPER_SPREADSHEET_ID=1abc... SKIPPER_CREDENTIALS_FILE=./service-account.json dotnet test| Column | Required | Description |
|---|---|---|
testId |
Yes | Canonical test identifier |
disabledUntil |
No | ISO-8601 date (2026-06-01). Empty = enabled |
notes |
No | Free text |
| Framework | Format |
|---|---|
| xUnit / NUnit / MSTest | Tests/Unit/AuthTests.cs > AuthTests > CanLogin |
| Playwright | Tests/E2E/LoginTests.cs > LoginTests > UserCanLogin |
| Reqnroll | Features/Auth/Login.feature > User authentication > User can log in |
Reconcile the spreadsheet with your test suite (append new tests, remove deleted ones):
SKIPPER_MODE=sync dotnet testTypically run on main after all tests pass:
# .github/workflows/ci.yml
- name: Run tests in sync mode
if: github.ref == 'refs/heads/main'
env:
SKIPPER_MODE: sync
SKIPPER_SPREADSHEET_ID: ${{ secrets.SKIPPER_SPREADSHEET_ID }}
GOOGLE_CREDS_B64: ${{ secrets.GOOGLE_CREDS_B64 }}
run: dotnet testThree ways to supply the Google service account:
// 1. JSON file (local dev)
Credentials = new FileCredentials("./service-account.json")
// 2. Base-64 encoded JSON (CI secrets)
Credentials = new Base64Credentials(Environment.GetEnvironmentVariable("GOOGLE_CREDS_B64")!)
// 3. Inline object
Credentials = new ServiceAccountCredentials(
clientEmail: "skipper@project.iam.gserviceaccount.com",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...")| Variable | Default | Description |
|---|---|---|
SKIPPER_MODE |
read-only |
Set to sync to reconcile spreadsheet |
SKIPPER_SPREADSHEET_ID |
— | Spreadsheet ID (used with SpreadsheetIdEnvVar) |
GOOGLE_CREDS_B64 |
— | Base-64 credentials (used with CredentialsEnvVar) |
SKIPPER_DEBUG |
— | Set to any value for verbose logging |
MIT — see LICENSE.