Troubleshooting & FAQ
This guide addresses common issues, configuration pitfalls, and debugging techniques for TinyBDD scenarios.
Common Errors
"TinyBDD ambient ScenarioContext not set"
Symptom: Exception thrown when calling Given(), When(), or Then() without context.
Cause: Using the ambient API (Flow methods) without setting up the ambient context.
Solutions:
- Inherit from a base class (recommended):
// Good: Base class sets up ambient context
public class MyTests : TinyBddXunitBase
{
public MyTests(ITestOutputHelper output) : base(output) { }
[Scenario("Works with ambient"), Fact]
public async Task WorksWithAmbient()
{
await Given(() => 1) // Ambient context is set
.When("double", x => x * 2)
.Then("equals 2", x => x == 2)
.AssertPassed();
}
}
- Set ambient context manually:
// Alternative: Manually set ambient context
public class MyTests
{
[Scenario("Manual ambient setup"), Fact]
public async Task ManualAmbientSetup()
{
var ctx = Bdd.CreateContext(this);
Ambient.Current.Value = ctx;
await Given(() => 1)
.When("double", x => x * 2)
.Then("equals 2", x => x == 2)
.AssertPassed();
}
}
- Use explicit context (no ambient):
// Alternative: Use explicit context API
public class MyTests
{
[Scenario("Explicit context"), Fact]
public async Task ExplicitContext()
{
var ctx = Bdd.CreateContext(this);
await Bdd.Given(ctx, "number", () => 1)
.When("double", x => x * 2)
.Then("equals 2", x => x == 2)
.AssertPassed();
}
}
"Assertion failed" with No Details
Symptom: Assertion fails but error message lacks context.
Cause: Using boolean predicate without descriptive title.
Solution: Use fluent expectations for better error messages:
// Avoid: Minimal error context
await Given(() => user)
.Then("user valid", u => u.Email.Contains("@"))
.AssertPassed();
// Error: "Assertion failed: user valid"
// Better: Fluent expectation with context
await Given(() => user)
.Then("email is valid", u =>
Expect.For(u.Email, "user email")
.Because("all users must have valid email addresses")
.ToSatisfy(email => email.Contains("@"), "contain @ symbol"))
.AssertPassed();
// Error: "expected user email to contain @ symbol, but was 'invalidemail' because all users must have valid email addresses"
Scenario Steps Not Appearing in Test Output
Symptom: Test passes/fails but BDD steps don't show in output.
Cause: Reporter not configured or base class not used.
Solutions:
- Inherit from framework base class:
// Ensures reporter is configured
public class MyTests : TinyBddXunitBase
{
public MyTests(ITestOutputHelper output) : base(output) { }
}
- Manual reporter setup:
[Fact]
public async Task ManualReporting()
{
var ctx = Bdd.CreateContext(this);
await Bdd.Given(ctx, "number", () => 1)
.When("double", x => x * 2)
.Then("equals 2", x => x == 2)
.AssertPassed();
// Manually write report
var reporter = new StringBddReporter();
GherkinFormatter.Write(ctx, reporter);
_output.WriteLine(reporter.ToString());
}
Tags Don't Appear as Traits/Categories
Symptom: [Tag] attributes don't show up in test framework's trait/category filters.
Cause: Most frameworks require native attributes for discovery-time filtering.
Solution: Use both TinyBDD tags and framework-specific attributes:
// xUnit
[Scenario("Smoke test")]
[Tag("smoke")] // For TinyBDD reporting
[Trait("Category", "smoke")] // For xUnit filtering
[Fact]
public async Task SmokeTest() { /* ... */ }
// NUnit
[Scenario("Smoke test")]
[Tag("smoke")] // For TinyBDD reporting
[Category("smoke")] // For NUnit filtering
[Test]
public async Task SmokeTest() { /* ... */ }
// MSTest
[Scenario("Smoke test")]
[Tag("smoke")] // For TinyBDD reporting
[TestCategory("smoke")] // For MSTest filtering
[TestMethod]
public async Task SmokeTest() { /* ... */ }
Parallel Test Failures
Symptom: Tests pass individually but fail when run in parallel.
Cause: Shared mutable state or ambient context conflicts.
Solutions:
- Avoid shared mutable state:
// Bad: Shared mutable state
private static int _counter = 0;
[Fact]
public async Task Test1()
{
_counter++; // Race condition!
await Given(() => _counter)
.Then("equals 1", x => x == 1) // May fail
.AssertPassed();
}
// Good: Isolated state
[Fact]
public async Task Test1()
{
var counter = 0;
counter++;
await Given(() => counter)
.Then("equals 1", x => x == 1)
.AssertPassed();
}
Use AsyncLocal for ambient context: TinyBDD's
Ambient.CurrentusesAsyncLocal, so each test has isolated context.Configure parallel execution:
<!-- xUnit: Disable parallelization if needed -->
<ItemGroup>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
{
"parallelizeTestCollections": false
}
Finally Blocks Not Executing
Symptom: Finally handlers don't run or resources aren't cleaned up.
Cause: Exception thrown before Finally handlers are registered or pipeline doesn't complete.
Solution: Ensure Finally is registered early:
// Bad: Finally might not register if Given throws
await Given("resource", () => CreateResourceThatMightThrow())
.Finally("cleanup", r => r.Dispose())
.When("use", r => r.DoSomething())
.Then("works", r => true)
.AssertPassed();
// Better: Register Finally immediately after resource creation
await Given("resource", () =>
{
var resource = CreateResource();
return resource;
})
.Finally("cleanup", r => r.Dispose()) // Registered before any potentially failing steps
.When("use", r => r.DoSomething())
.Then("works", r => true)
.AssertPassed();
Configuration Issues
Test Discovery Problems
Symptom: Tests don't appear in Test Explorer.
Causes and Solutions:
- Missing test framework packages:
# Ensure test adapter is installed
dotnet add package xunit.runner.visualstudio
# or
dotnet add package NUnit3TestAdapter
# or
dotnet add package MSTest.TestAdapter
- Incorrect test attributes:
// xUnit requires [Fact] or [Theory]
[Scenario("Test"), Fact]
public async Task Test() { /* ... */ }
// NUnit requires [Test]
[Scenario("Test"), Test]
public async Task Test() { /* ... */ }
// MSTest requires [TestMethod] and class must have [TestClass]
[TestClass]
public class Tests : TinyBddMsTestBase
{
[Scenario("Test"), TestMethod]
public async Task Test() { /* ... */ }
}
- Build issues: Clean and rebuild the solution:
dotnet clean
dotnet build
Output Not Showing in CI/CD
Symptom: BDD output visible locally but not in CI logs.
Solution: Use appropriate verbosity and loggers:
# GitHub Actions
dotnet test --verbosity normal --logger "console;verbosity=detailed"
# Azure DevOps
dotnet test --logger "trx" --logger "console;verbosity=normal"
# GitLab CI
dotnet test --logger "junit;LogFilePath=test-results.xml"
Scenario Outline Examples Not Running
Symptom: ScenarioOutline doesn't execute examples or all examples fail.
Common Causes:
- Forgot to call
AssertAllPassedAsync():
// Bad: Examples won't run
var outline = await Bdd.ScenarioOutline<int>(ctx, "Test")
.Given("number", ex => ex)
.Then("positive", n => n > 0)
.Examples(1, 2, 3);
// Missing AssertAllPassedAsync() or RunAsync()
// Good: Examples execute
await Bdd.ScenarioOutline<int>(ctx, "Test")
.Given("number", ex => ex)
.Then("positive", n => n > 0)
.Examples(1, 2, 3)
.AssertAllPassedAsync(); // Executes all examples
- No examples provided:
// Error: InvalidOperationException
await Bdd.ScenarioOutline<int>(ctx, "Test")
.Given("number", ex => ex)
.Then("positive", n => n > 0)
.AssertAllPassedAsync(); // Throws: No examples provided
Debugging Techniques
Enable Diagnostic Output
Add diagnostic logging to understand execution flow:
[Scenario("Debug scenario"), Fact]
public async Task DebugScenario()
{
var ctx = Bdd.CreateContext(this);
// Log context state
_output.WriteLine($"Feature: {ctx.FeatureName}");
_output.WriteLine($"Scenario: {ctx.ScenarioName}");
await Bdd.Given(ctx, "value", () =>
{
var value = 1;
_output.WriteLine($"Given: value={value}");
return value;
})
.When("doubled", x =>
{
var result = x * 2;
_output.WriteLine($"When: {x} * 2 = {result}");
return result;
})
.Then("equals 2", x =>
{
_output.WriteLine($"Then: checking {x} == 2");
return x == 2;
})
.AssertPassed();
// Log steps
foreach (var step in ctx.Steps)
{
_output.WriteLine($"{step.Kind} {step.Title} [{(step.Error == null ? "OK" : "FAIL")}] {step.Elapsed.TotalMilliseconds}ms");
}
}
Inspect Step IO
Examine data flow through scenario:
[Scenario("Inspect data flow"), Fact]
public async Task InspectDataFlow()
{
var ctx = Bdd.CreateContext(this);
await Bdd.Given(ctx, "initial", () => 1)
.When("transform", x => x * 2)
.Then("verify", x => x == 2)
.AssertPassed();
// Examine IO lineage
foreach (var io in ctx.IO)
{
_output.WriteLine($"{io.Kind} {io.Title}:");
_output.WriteLine($" Input: {io.Input ?? "<null>"}");
_output.WriteLine($" Output: {io.Output ?? "<null>"}");
}
// Check current item
_output.WriteLine($"Current Item: {ctx.CurrentItem}");
}
Use Breakpoints Effectively
Set breakpoints in step lambdas:
[Scenario("Debug with breakpoints"), Fact]
public async Task DebugWithBreakpoints()
{
await Given("value", () =>
{
var x = 1; // Breakpoint here
return x;
})
.When("transform", x =>
{
var result = x * 2; // Breakpoint here
return result;
})
.Then("verify", x =>
{
var isValid = x == 2; // Breakpoint here
return isValid;
})
.AssertPassed();
}
Isolate Failing Steps
Comment out steps to isolate failures:
[Scenario("Isolate failure"), Fact]
public async Task IsolateFailure()
{
await Given("setup", () => Setup())
.And("more setup", (state, ct) => MoreSetup(state))
// .When("action that fails", x => FailingAction(x)) // Commented out
.Then("verify", x => x != null)
.AssertPassed();
}
Capture Exceptions
Understand exception details:
[Scenario("Exception details"), Fact]
public async Task ExceptionDetails()
{
var ctx = Bdd.CreateContext(this);
try
{
await Bdd.Given(ctx, "setup", () => 1)
.When("failing step", x => throw new InvalidOperationException("Test error"))
.Then("never reached", x => x == 1)
.AssertPassed();
}
catch (BddStepException ex)
{
_output.WriteLine($"Step failed: {ex.Message}");
_output.WriteLine($"Inner exception: {ex.InnerException?.Message}");
_output.WriteLine($"Context: Feature={ctx.FeatureName}, Scenario={ctx.ScenarioName}");
// Examine steps
var failedStep = ctx.Steps.FirstOrDefault(s => s.Error != null);
if (failedStep != null)
{
_output.WriteLine($"Failed step: {failedStep.Kind} {failedStep.Title}");
_output.WriteLine($"Error: {failedStep.Error.Message}");
}
}
}
Performance Issues
Slow Test Execution
Symptom: Tests take longer than expected.
Diagnosis:
- Check step timing:
var slowSteps = ctx.Steps.Where(s => s.Elapsed.TotalMilliseconds > 100);
foreach (var step in slowSteps)
{
_output.WriteLine($"Slow step: {step.Kind} {step.Title} took {step.Elapsed.TotalMilliseconds}ms");
}
- Profile setup/teardown:
private readonly Stopwatch _setupTimer = new();
[SetUp]
public void Setup()
{
_setupTimer.Restart();
// Setup code
_setupTimer.Stop();
Console.WriteLine($"Setup took {_setupTimer.ElapsedMilliseconds}ms");
}
Solutions:
- Optimize expensive operations:
// Bad: Expensive operation in every test
[SetUp]
public void Setup()
{
_db = new TestDatabase();
_db.SeedWithMillionsOfRecords(); // Very slow
}
// Good: Use cached or minimal data
private static TestDatabase _sharedDb;
[OneTimeSetUp]
public void OneTimeSetup()
{
_sharedDb = new TestDatabase();
_sharedDb.SeedWithMinimalData();
}
Use parallel execution (see Configuration Issues above)
Tag slow tests:
[Scenario("Slow integration test")]
[Tag("slow")]
[Tag("integration")]
[Fact]
public async Task SlowIntegrationTest() { /* ... */ }
# Run fast tests only
dotnet test --filter "Category!=slow"
High Memory Usage
Symptom: Tests consume excessive memory.
Causes and Solutions:
- Large test data: Use smaller data sets or streaming:
// Bad: Loading huge dataset
var data = LoadMillionRecords();
// Good: Use representative subset
var data = LoadSampleRecords(100);
- Resource leaks: Ensure proper disposal:
// Always use Finally for cleanup
await Given("resource", () => new ExpensiveResource())
.Finally("dispose", r => r.Dispose())
.When("use", r => r.DoWork())
.Then("succeeds", r => true)
.AssertPassed();
Framework-Specific Issues
xUnit: ITestOutputHelper is Null
Symptom: NullReferenceException when writing to ITestOutputHelper.
Cause: Base class constructor not called or parameter not passed.
Solution:
public class MyTests : TinyBddXunitBase
{
// Good: Pass output to base constructor
public MyTests(ITestOutputHelper output) : base(output)
{
}
}
NUnit: TestContext is Null
Symptom: NullReferenceException when accessing TestContext.
Cause: Accessing TestContext outside of test execution.
Solution: Only access TestContext within test methods or SetUp/TearDown:
[SetUp]
public void Setup()
{
var context = TestContext.CurrentContext; // OK here
}
[Test]
public async Task MyTest()
{
var context = TestContext.CurrentContext; // OK here
}
// Avoid accessing TestContext in constructor or OneTimeSetUp
MSTest: TestContext Not Available
Symptom: TestContext is null or not set.
Cause: TestContext property not defined or not public.
Solution:
[TestClass]
public class MyTests : TinyBddMsTestBase
{
// TestContext must be a public property named exactly "TestContext"
public TestContext TestContext { get; set; }
[TestMethod]
public async Task MyTest()
{
// TestContext.WriteLine available here
}
}
FAQ
Q: Can I use TinyBDD without inheriting a base class?
A: Yes, use the explicit context API:
public class MyTests
{
[Fact]
public async Task ExplicitContext()
{
var ctx = Bdd.CreateContext(this);
await Bdd.Given(ctx, "value", () => 1)
.When("double", x => x * 2)
.Then("equals 2", x => x == 2)
.AssertPassed();
}
}
Q: How do I test async code?
A: TinyBDD supports async natively. All step methods have async overloads:
await Given("async setup", async () => await LoadDataAsync())
.When("async action", async data => await ProcessAsync(data))
.Then("async verify", async result => await VerifyAsync(result))
.AssertPassed();
Q: Can I mix TinyBDD with regular assertions?
A: Yes, use action-based Then steps:
await Given(() => user)
.Then("user is valid", u =>
{
Assert.NotNull(u); // Regular assertion
Assert.True(u.IsActive);
u.Email.Should().Contain("@"); // FluentAssertions
})
.AssertPassed();
Q: How do I handle expected exceptions?
A: Use AssertFailed() or catch the exception:
// Expect the scenario to fail
await Given(() => 0)
.When("divide", x => 10 / x) // Throws DivideByZeroException
.Then("never reached", x => true)
.AssertFailed();
Q: Can I reuse step definitions?
A: Yes, create helper methods:
private async Task<ScenarioChain<User>> GivenAuthenticatedUser()
{
return await Given("authenticated user", async () =>
{
var user = await _authService.CreateUserAsync();
await _authService.LoginAsync(user);
return user;
});
}
[Scenario("User can edit profile"), Fact]
public async Task UserCanEditProfile()
{
await GivenAuthenticatedUser()
.When("updating profile", u => _profileService.UpdateAsync(u, newData))
.Then("profile updated", result => result.Success)
.AssertPassed();
}
Q: How do I test multiple scenarios with shared setup?
A: Use test class initialization:
public class SharedSetupTests : TinyBddXunitBase, IClassFixture<DatabaseFixture>
{
private readonly DatabaseFixture _fixture;
public SharedSetupTests(ITestOutputHelper output, DatabaseFixture fixture)
: base(output)
{
_fixture = fixture;
}
[Scenario("Test 1"), Fact]
public async Task Test1()
{
await Given("shared db", () => _fixture.Database)
.When("query", db => db.Query<User>())
.Then("users exist", users => users.Any())
.AssertPassed();
}
[Scenario("Test 2"), Fact]
public async Task Test2()
{
await Given("shared db", () => _fixture.Database)
.When("query", db => db.Query<Order>())
.Then("orders exist", orders => orders.Any())
.AssertPassed();
}
}
Getting Help
If you encounter an issue not covered here:
- Check the GitHub repository: Search existing issues and discussions
- Review sample projects: Look at test projects in the repository
- Read the API documentation: Explore the generated API docs
- Create an issue: Provide a minimal reproducible example
Next Steps
- Explore Advanced Usage for advanced patterns
- See Samples Index for working examples
- Review Writing Scenarios for best practices