Skip to main content

Tutorial: Building a Shipping Costs Test Suite

Docs for xUnit SDK v0.4.0

In this tutorial you'll build a complete, real-world test suite for an online tea shop's shipping calculator. You'll use every LiveDoc feature you've learned — features, scenarios, scenario outlines, value extraction, async steps, and descriptions — all in one cohesive test class.

What You'll Build​

A comprehensive test suite for these business rules:

RuleDetails
Australian GST10% added to all domestic orders
No GST overseasInternational orders are GST-free
Free shippingAustralian orders ≥ $100
Standard DomesticAustralian orders < $100 → $9.95
InternationalAll overseas orders → $25.00 flat

By the end, you'll have 5 test methods covering individual scenarios, edge cases, and data-driven outlines — all producing beautiful living documentation.


Prerequisites​


Step 1: Set Up the Domain​

First, create the classes under test. In a real project these would be in your application code — here we'll define them alongside the tests for simplicity.

Create ShoppingCart.cs:

namespace TeaShop;

public class CartItem
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}

public class ShoppingCart
{
public string Country { get; set; } = "Australia";
public List<CartItem> Items { get; } = new();
public decimal Subtotal => Items.Sum(i => i.Price);
public decimal GST { get; private set; }
public decimal Shipping { get; private set; }
public string ShippingType { get; private set; } = "";

public void AddItem(CartItem item) => Items.Add(item);

public void Calculate()
{
// GST: 10% for Australian customers only
GST = Country == "Australia" ? Math.Round(Subtotal * 0.10m, 3) : 0m;

// Shipping rules
if (Country == "Australia")
{
if (Subtotal >= 100m)
{
Shipping = 0m;
ShippingType = "Free";
}
else
{
Shipping = 9.95m;
ShippingType = "Standard Domestic";
}
}
else
{
Shipping = 25.00m;
ShippingType = "Standard International";
}
}

public Task CalculateAsync()
{
Calculate();
return Task.CompletedTask;
}
}

Step 2: Create the Feature Class​

Create ShippingCostsTests.cs with the feature container and constructor:

using SweDevTools.LiveDoc.xUnit;
using SweDevTools.LiveDoc.xUnit.Core;
using Xunit;
using Xunit.Abstractions;
using TeaShop;

namespace TeaShop.Tests.Shipping;

[Feature("Beautiful Tea Shipping Costs", Description = @"
Avoids incorrect tax amounts and delivery tiers for
the destinations and totals tested below.
")]
public class ShippingCostsTests : FeatureTest
{
private ShoppingCart _cart = null!;

public ShippingCostsTests(ITestOutputHelper output) : base(output)
{
}
}

The Description tells readers why these calculations matter; the scenario steps and example rows below show the actual thresholds and results.


Step 3: Add the First Scenario — Free Shipping​

The simplest case: an Australian customer ordering $100 or more:

[Scenario(nameof(Free_shipping_in_Australia))]
public void Free_shipping_in_Australia()
{
Given("the customer is from Australia", () =>
{
_cart = new ShoppingCart { Country = "Australia" };
});

When("the customer's order totals $100", () =>
{
_cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = 100.00m });
_cart.Calculate();
});

Then("the customer pays GST", () =>
{
Assert.Equal(10.00m, _cart.GST);
});

And("they are charged Free shipping", () =>
{
Assert.Equal(0m, _cart.Shipping);
Assert.Equal("Free", _cart.ShippingType);
});
}
Using nameof

Passing nameof(Free_shipping_in_Australia) as the scenario title ensures the display name stays in sync with the method name. Underscores are automatically converted to spaces in the output.


Step 4: Add a Contrasting Scenario — Under $100​

Now the domestic case where shipping is charged:

[Scenario(nameof(Standard_shipping_in_Australia_for_orders_under_100_dollars))]
public void Standard_shipping_in_Australia_for_orders_under_100_dollars()
{
Given("the customer is from Australia", () =>
{
_cart = new ShoppingCart { Country = "Australia" };
});

When("the customer's order totals $99.99", () =>
{
_cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = 99.99m });
_cart.Calculate();
});

Then("the customer pays GST", () =>
{
Assert.Equal(9.999m, Math.Round(_cart.GST, 3));
});

And("they are charged Standard Domestic shipping", () =>
{
Assert.Equal(9.95m, _cart.Shipping);
Assert.Equal("Standard Domestic", _cart.ShippingType);
});
}

Step 5: Add the International Scenario​

Overseas customers get a different shipping rate and no GST:

[Scenario(nameof(International_shipping_for_overseas_customers))]
public void International_shipping_for_overseas_customers()
{
Given("the customer is from New Zealand", () =>
{
_cart = new ShoppingCart { Country = "New Zealand" };
});

When("the customer's order totals $100", () =>
{
_cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = 100.00m });
_cart.Calculate();
});

Then("the customer pays no GST", () =>
{
Assert.Equal(0m, _cart.GST);
});

And("they are charged Standard International shipping", () =>
{
Assert.Equal(25.00m, _cart.Shipping);
Assert.Equal("Standard International", _cart.ShippingType);
});
}

Step 6: Add a Scenario Outline for Comprehensive Coverage​

Individual scenarios document key cases, but a Scenario Outline lets you cover all combinations in one structure. This is the most powerful pattern for data-driven testing:

[ScenarioOutline(nameof(Calculate_GST_and_shipping),
Description = @"
Covers the listed Australian and overseas destinations;
other regions need their own examples.
")]
[Example("Australia", 99.99, 9.999, "Standard Domestic")]
[Example("Australia", 100.00, 10.00, "Free")]
[Example("New Zealand", 99.99, 0, "Standard International")]
[Example("New Zealand", 100.00, 0, "Standard International")]
[Example("Zimbabwe", 100.00, 0, "Standard International")]
public void Calculate_GST_and_shipping(
string CustomerCountry,
decimal OrderTotal,
decimal ExpectedGST,
string ExpectedShippingRate)
{
Given("the customer is from <CustomerCountry>", () =>
{
_cart = new ShoppingCart { Country = CustomerCountry };
});

When("the customer's order totals <OrderTotal>", () =>
{
_cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = OrderTotal });
_cart.Calculate();
});

Then("the customer pays <ExpectedGST> GST", () =>
{
Assert.Equal(ExpectedGST, Math.Round(_cart.GST, 3));
});

And("they are charged the <ExpectedShippingRate> shipping rate", () =>
{
Assert.Equal(ExpectedShippingRate, _cart.ShippingType);
});
}

Each [Example] row generates a fully independent test run. The <Placeholder> values in the step titles are automatically replaced with the actual data in the formatted output.


Step 7: Add an Async Scenario​

Real-world code is often asynchronous. LiveDoc handles async steps naturally:

[Scenario("Async shipping calculation")]
public async Task Async_shipping_test()
{
ShoppingCart? cart = null;

await Given("a customer from Australia with $150 order", async () =>
{
await Task.Delay(10); // Simulate async setup
cart = new ShoppingCart { Country = "Australia" };
cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = 150.00m });
});

await When("we calculate shipping asynchronously", async () =>
{
await cart!.CalculateAsync();
});

Then("the shipping should be Free", () =>
{
Assert.Equal("Free", cart!.ShippingType);
});

And("GST should be calculated correctly", () =>
{
Assert.Equal(15.00m, cart!.GST);
});
}

Notice how sync and async steps mix freely — just await the async ones.


Step 8: Run and Admire the Output​

dotnet test --logger "console;verbosity=detailed"

The complete output reads like a specification document:

Feature: Beautiful Tea Shipping Costs

Scenario: Free shipping in Australia
Given the customer is from Australia
When the customer's order totals $100
Then the customer pays GST
And they are charged Free shipping

✓ 4 passing (15ms)

Scenario: Standard shipping in Australia for orders under 100 dollars
Given the customer is from Australia
When the customer's order totals $99.99
Then the customer pays GST
And they are charged Standard Domestic shipping

✓ 4 passing (8ms)

Scenario: International shipping for overseas customers
Given the customer is from New Zealand
When the customer's order totals $100
Then the customer pays no GST
And they are charged Standard International shipping

✓ 4 passing (7ms)

Scenario Outline: Calculate GST and shipping
Example: Australia, 99.99, 9.999, Standard Domestic
Given the customer is from Australia
When the customer's order totals 99.99
Then the customer pays 9.999 GST
And they are charged the Standard Domestic shipping rate
✓ 4 passing (5ms)

Example: Australia, 100.00, 10.00, Free
Given the customer is from Australia
When the customer's order totals 100.00
Then the customer pays 10.00 GST
And they are charged the Free shipping rate
✓ 4 passing (4ms)

...

Scenario: Async shipping calculation
Given a customer from Australia with $150 order
When we calculate shipping asynchronously
Then the shipping should be Free
And GST should be calculated correctly

✓ 4 passing (25ms)

This output is the specification. Share it with stakeholders, include it in documentation, or view it in the LiveDoc Viewer.


The Complete Test Class​

Here's everything assembled into the final file:

using SweDevTools.LiveDoc.xUnit;
using SweDevTools.LiveDoc.xUnit.Core;
using Xunit;
using Xunit.Abstractions;
using TeaShop;

namespace TeaShop.Tests.Shipping;

[Feature("Beautiful Tea Shipping Costs", Description = @"
Avoids incorrect tax amounts and delivery tiers for
the destinations and totals tested below.
")]
public class ShippingCostsTests : FeatureTest
{
private ShoppingCart _cart = null!;

public ShippingCostsTests(ITestOutputHelper output) : base(output)
{
}

[Scenario(nameof(Free_shipping_in_Australia))]
public void Free_shipping_in_Australia()
{
Given("the customer is from Australia", () =>
{
_cart = new ShoppingCart { Country = "Australia" };
});

When("the customer's order totals $100", () =>
{
_cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = 100.00m });
_cart.Calculate();
});

Then("the customer pays GST", () =>
{
Assert.Equal(10.00m, _cart.GST);
});

And("they are charged Free shipping", () =>
{
Assert.Equal(0m, _cart.Shipping);
Assert.Equal("Free", _cart.ShippingType);
});
}

[Scenario(nameof(Standard_shipping_in_Australia_for_orders_under_100_dollars))]
public void Standard_shipping_in_Australia_for_orders_under_100_dollars()
{
Given("the customer is from Australia", () =>
{
_cart = new ShoppingCart { Country = "Australia" };
});

When("the customer's order totals $99.99", () =>
{
_cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = 99.99m });
_cart.Calculate();
});

Then("the customer pays GST", () =>
{
Assert.Equal(9.999m, Math.Round(_cart.GST, 3));
});

And("they are charged Standard Domestic shipping", () =>
{
Assert.Equal(9.95m, _cart.Shipping);
Assert.Equal("Standard Domestic", _cart.ShippingType);
});
}

[Scenario(nameof(International_shipping_for_overseas_customers))]
public void International_shipping_for_overseas_customers()
{
Given("the customer is from New Zealand", () =>
{
_cart = new ShoppingCart { Country = "New Zealand" };
});

When("the customer's order totals $100", () =>
{
_cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = 100.00m });
_cart.Calculate();
});

Then("the customer pays no GST", () =>
{
Assert.Equal(0m, _cart.GST);
});

And("they are charged Standard International shipping", () =>
{
Assert.Equal(25.00m, _cart.Shipping);
Assert.Equal("Standard International", _cart.ShippingType);
});
}

[ScenarioOutline(nameof(Calculate_GST_and_shipping))]
[Example("Australia", 99.99, 9.999, "Standard Domestic")]
[Example("Australia", 100.00, 10.00, "Free")]
[Example("New Zealand", 99.99, 0, "Standard International")]
[Example("New Zealand", 100.00, 0, "Standard International")]
[Example("Zimbabwe", 100.00, 0, "Standard International")]
public void Calculate_GST_and_shipping(
string CustomerCountry,
decimal OrderTotal,
decimal ExpectedGST,
string ExpectedShippingRate)
{
Given("the customer is from <CustomerCountry>", () =>
{
_cart = new ShoppingCart { Country = CustomerCountry };
});

When("the customer's order totals <OrderTotal>", () =>
{
_cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = OrderTotal });
_cart.Calculate();
});

Then("the customer pays <ExpectedGST> GST", () =>
{
Assert.Equal(ExpectedGST, Math.Round(_cart.GST, 3));
});

And("they are charged the <ExpectedShippingRate> shipping rate", () =>
{
Assert.Equal(ExpectedShippingRate, _cart.ShippingType);
});
}

[Scenario("Async shipping calculation")]
public async Task Async_shipping_test()
{
ShoppingCart? cart = null;

await Given("a customer from Australia with $150 order", async () =>
{
await Task.Delay(10);
cart = new ShoppingCart { Country = "Australia" };
cart.AddItem(new CartItem { Name = "Byron Breakfast Tea", Price = 150.00m });
});

await When("we calculate shipping asynchronously", async () =>
{
await cart!.CalculateAsync();
});

Then("the shipping should be Free", () =>
{
Assert.Equal("Free", cart!.ShippingType);
});

And("GST should be calculated correctly", () =>
{
Assert.Equal(15.00m, cart!.GST);
});
}
}

What You've Learned​

Across this tutorial and the entire Learn series, you've mastered:

  • [Feature] + [Scenario] — BDD testing with Given/When/Then
  • [Specification] + [Rule] — concise unit testing
  • [ScenarioOutline] + [Example] — data-driven BDD tests
  • [RuleOutline] + [Example] — data-driven specification tests
  • Value extraction — ctx.Step.Values, Rule.Values, Params[], As<T>()
  • Descriptions — adding context to attributes
  • Async steps — mixing sync and async freely
  • Namespace organization — structuring tests for clear reporting

Where to Go Next​

You've completed the Learn series! Here's where to continue: