Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions demo/Tests/001-Customers/001-Add-Customer-test.csx
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,19 @@ await tp.Test("Customer should be created successfully.", async () =>
// var body = tp.Request.GetBody().ToJson();
// var id = (long)body["Id"];
});

await tp.Test("Customer data should be valid.", async () =>
{
dynamic customer = await tp.Request.GetBodyAsExpandoAsync();

// TeaPie extends Xunit.Assert with additional assertion methods for more expressive tests.
// These are C# 14 extension members on Assert, callable via 'Assert.' prefix.
// Use NotNullOrEmpty to verify that a string value is present.
Assert.NotNullOrEmpty((string)customer.firstName);
Assert.NotNullOrEmpty((string)customer.lastName);
Assert.NotNullOrEmpty((string)customer.email);

// Use GreaterThan to verify that a numeric value exceeds a threshold.
// Following xUnit convention: first argument is the expected limit, second is the actual value.
Assert.GreaterThan(0L, (long)customer.id);
});
2 changes: 2 additions & 0 deletions docs/docs/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@
href: commands.md
- name: Directives
href: directives.md
- name: Xunit.Assert Extensions
href: xunit-assert-extensions.md
144 changes: 144 additions & 0 deletions docs/docs/xunit-assert-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Xunit.Assert Extensions

TeaPie extends the default `Xunit.Assert` with additional assertion methods to make your tests more expressive and provide better failure messages.

These methods are added directly to `Xunit.Assert` using **C# 14 extension types**, so they can be called with the standard `Assert.` prefix — or without it, since `Xunit.Assert` is **statically imported** in all script files.

> 💡 Following the xUnit convention, the **first argument is the expected boundary** (limit/threshold) and the **second argument is the actual value** being verified.

## Comparison Assertions

These methods work for **any type implementing `IComparable<T>`** (e.g., `int`, `long`, `DateTime`, `string`, etc.).
For floating-point types (`double` and `float`), additional overloads with an **epsilon** parameter are provided to account for floating-point imprecision.

### GreaterThan

Verifies that `value` is greater than `limit`.

```csharp
Assert.GreaterThan(limit, value);
Assert.GreaterThan(limit, value, epsilon); // For double and float
```

**Example:**

```csharp
tp.Test("Response time should be acceptable.", () =>
{
var responseTimeMs = tp.Response.Headers.Age?.TotalMilliseconds ?? 0;
Assert.GreaterThan(0, responseTimeMs); // More readable than: True(responseTimeMs > 0)
});
```

### GreaterThanOrEqual

Verifies that `value` is greater than or equal to `limit`.

```csharp
Assert.GreaterThanOrEqual(limit, value);
Assert.GreaterThanOrEqual(limit, value, epsilon); // For double and float
```

### LessThan

Verifies that `value` is less than `limit`.

```csharp
Assert.LessThan(limit, value);
Assert.LessThan(limit, value, epsilon); // For double and float
```

**Example:**

```csharp
tp.Test("Status code should be a client error.", () =>
{
var statusCode = tp.Response.StatusCode();
Assert.GreaterThanOrEqual(400, statusCode);
Assert.LessThan(500, statusCode);
});
```

### LessThanOrEqual

Verifies that `value` is less than or equal to `limit`.

```csharp
Assert.LessThanOrEqual(limit, value);
Assert.LessThanOrEqual(limit, value, epsilon); // For double and float
```

### Epsilon Parameter

When comparing `double` or `float` values, an **epsilon** parameter can be specified to define the maximum allowed difference for two values to be considered equal. This is useful when dealing with floating-point arithmetic imprecision.

```csharp
tp.Test("Price should be within acceptable range.", () =>
{
var price = tp.GetVariable<double>("ItemPrice");
Assert.GreaterThan(0.0, price, 0.001); // price must be > 0.001
Assert.LessThan(1000.0, price, 0.001); // price must be < 999.999
});
```

## Null or Empty Assertions

These methods apply to both **strings** and **collections**.

### NullOrEmpty

Verifies that a string or collection is `null` or empty.

```csharp
Assert.NullOrEmpty(value); // string
Assert.NullOrEmpty(collection); // IEnumerable<T>
```

**Example:**

```csharp
tp.Test("Error list should be empty.", () =>
{
var errors = tp.GetVariable<string[]>("ValidationErrors");
Assert.NullOrEmpty(errors);
});
```

### NotNullOrEmpty

Verifies that a string or collection is **not** `null` and **not** empty.

```csharp
Assert.NotNullOrEmpty(value); // string
Assert.NotNullOrEmpty(collection); // IEnumerable<T>
```

**Example:**

```csharp
await tp.Test("Response body should not be empty.", async () =>
{
var body = await tp.Response.GetBodyAsStringAsync();
Assert.NotNullOrEmpty(body);
});
```

## JsonContains

Verifies that a JSON string contains another JSON object. Optionally, specific properties can be excluded from the comparison.

```csharp
JsonContains(container, contained);
JsonContains(container, contained, "propertyToIgnore", "anotherProperty");
```

**Example:**

```csharp
await tp.Test("Response should contain the expected customer data.", async () =>
{
var responseBody = await tp.Response.GetBodyAsStringAsync();
var expected = """{ "name": "Alice", "age": 30 }""";
JsonContains(responseBody, expected);
});
```
3 changes: 2 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
12 changes: 6 additions & 6 deletions src/TeaPie/Pipelines/StepsCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ internal class StepsCollection : IEnumerable<IPipelineStep>

public void Insert(IPipelineStep predecessor, IPipelineStep step)
{
ArgumentNullException.ThrowIfNull(nameof(predecessor));
ArgumentNullException.ThrowIfNull(nameof(step));
ArgumentNullException.ThrowIfNull(predecessor);
ArgumentNullException.ThrowIfNull(step);

if (_index.TryGetValue(predecessor, out var referenceNode))
{
Expand All @@ -27,16 +27,16 @@ public void Insert(IPipelineStep predecessor, IPipelineStep step)

public void Add(IPipelineStep step)
{
ArgumentNullException.ThrowIfNull(nameof(step));
ArgumentNullException.ThrowIfNull(step);

var node = _steps.AddLast(step);
_index.Add(step, node);
}

public void InsertRange(IPipelineStep predecessor, IEnumerable<IPipelineStep> steps)
{
ArgumentNullException.ThrowIfNull(nameof(predecessor));
ArgumentNullException.ThrowIfNull(nameof(steps));
ArgumentNullException.ThrowIfNull(predecessor);
ArgumentNullException.ThrowIfNull(steps);

if (_index.TryGetValue(predecessor, out var referenceNode))
{
Expand All @@ -55,7 +55,7 @@ public void InsertRange(IPipelineStep predecessor, IEnumerable<IPipelineStep> st

public void AddRange(IEnumerable<IPipelineStep> steps)
{
ArgumentNullException.ThrowIfNull(nameof(steps));
ArgumentNullException.ThrowIfNull(steps);

foreach (var step in steps)
{
Expand Down
Loading
Loading