Skip to content

Commit

Permalink
feat: add a sample test project
Browse files Browse the repository at this point in the history
  • Loading branch information
ascott18 committed May 15, 2024
1 parent d03bbef commit 1946205
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 50 deletions.
3 changes: 3 additions & 0 deletions content/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ coverage/
# https://github.com/antfu/unplugin-vue-components/issues/611
components.d.ts

# Vite temporary compiled config files.
vite.config.ts.timestamp*

# User-specific files
*.suo
*.user
Expand Down
1 change: 0 additions & 1 deletion content/.vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"esbenp.prettier-vscode",
"antfu.goto-alias"
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq.AutoMock" Version="3.5.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Coalesce.Starter.Vue.Data\Coalesce.Starter.Vue.Data.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
<Using Include="Coalesce.Starter.Vue.Data" />
<Using Include="Coalesce.Starter.Vue.Data.Models" />
</ItemGroup>

</Project>
27 changes: 27 additions & 0 deletions content/Coalesce.Starter.Vue.Data.Test/UnitTest1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Coalesce.Starter.Vue.Data.Test;

public class UnitTest1 : TestBase
{
[Fact]
public void Test1()
{
// Arrange
var widget1 = new Widget { Name = "Gnoam Sprecklesprocket", Category = WidgetCategory.Sprecklesprockets };
Db.Add(widget1);
Db.SaveChanges();

RefreshServices();

// Act
var widget2 = Db.Widgets.Single();

// Assert
Assert.Equal(WidgetCategory.Sprecklesprockets, widget2.Category);

// After calling RefreshServices, we have a different DbContext instance
// and so we'll get a different entity instance.
Assert.NotEqual(widget1, widget2);


}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

namespace Coalesce.Starter.Vue.Data.Test;

public class AppDbContextForSqlite(DbContextOptions<AppDbContext> options) : AppDbContext(options)
{
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
configurationBuilder.Properties<DateTimeOffset>().HaveConversion<DateTimeOffsetToStringConverter>();
configurationBuilder.Properties<DateOnly>().HaveConversion<DateOnlyToDateTimeConverter>();
}

public class DateOnlyToDateTimeConverter : ValueConverter<DateOnly, DateTime>
{
public DateOnlyToDateTimeConverter()
: base(
dateOnly => dateOnly.ToDateTime(new TimeOnly()),
dateTime => DateOnly.FromDateTime(dateTime)
)
{ }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using IntelliTect.Coalesce.Models;

namespace Coalesce.Starter.Vue.Data.Test;

public static class AssertionExtensions
{
/// <summary>
/// Asserts that the result was a failure.
/// </summary>
public static void AssertError(this ApiResult result)
{
Assert.False(result.WasSuccessful);
}

/// <summary>
/// Asserts that the result was a failure.
/// </summary>
/// <param name="message">Expected error message.</param>
public static void AssertError(this ApiResult result, string message)
{
result.AssertError();
Assert.Equal(message, result.Message);
}

/// <summary>
/// Asserts that the result was successful.
/// </summary>
public static void AssertSuccess(this ApiResult result, string? message = null)
{
// Returns a more useful assertion error than only checking WasSuccessful.
Assert.Equal(message, result.Message);
Assert.True(result.WasSuccessful);
}

/// <summary>
/// Asserts that the result was successful.
/// </summary>
public static T AssertSuccess<T>(this ItemResult<T> result)
{
Assert.Null(result.Message);
Assert.True(result.WasSuccessful);
return result.Object ?? throw new ArgumentException("Sucessful result unexpectedly returned null object");
}

/// <summary>
/// Asserts that the result was successful.
/// </summary>
public static async Task<T> AssertSuccess<T>(this Task<ItemResult<T>> resultTask)
{
var result = await resultTask;
return result.AssertSuccess();
}

/// <summary>
/// Asserts that the result was successful.
/// </summary>
public static async Task AssertSuccess(this Task<ItemResult> resultTask)
{
var result = await resultTask;
Assert.True(result.WasSuccessful);
}

/// <summary>
/// Asserts that the result was successful.
/// </summary>
/// <param name="expectedValue">Expected value on the result.</param>
public static void AssertSuccess<T>(this ItemResult<T> result, T expectedValue)
{
result.AssertSuccess();
Assert.Equal(expectedValue, result.Object);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Coalesce.Starter.Vue.Data.Test;

public class SqliteDatabaseFixture : IDisposable
{
private readonly SqliteConnection _HoldOpenConnection;

public DbContextOptions<AppDbContext> Options { get; }

private static readonly ILoggerFactory _LoggerFac = LoggerFactory.Create(b =>
{
b.SetMinimumLevel(LogLevel.Error);
b.AddConsole();
});

public SqliteDatabaseFixture()
{
// Use a unique database instance per test.
var connString = $"Data Source=A{Guid.NewGuid():N};Mode=Memory;Cache=Shared";

// Per https://docs.microsoft.com/en-us/dotnet/standard/data/sqlite/in-memory-databases#shareable-in-memory-databases,
// a connection must be kept open in order to preserve a particular in-memory SQLite instance.
// EF doesn't hold connections open necessarily, so we'll do this ourselves.
_HoldOpenConnection = new SqliteConnection(connString);
_HoldOpenConnection.Open();

var dbOptionBuilder = new DbContextOptionsBuilder<AppDbContext>();
dbOptionBuilder.UseSqlite(connString);
dbOptionBuilder.UseLoggerFactory(_LoggerFac);
dbOptionBuilder.EnableDetailedErrors(true);
dbOptionBuilder.EnableSensitiveDataLogging(true);

Options = dbOptionBuilder.Options;
using var db = new AppDbContextForSqlite(Options);

db.Database.EnsureCreated();
Seed();
}

public void Seed()
{
// Seed baseline test data, if desired.
}

public void Dispose()
{
_HoldOpenConnection.Dispose();
}
}
115 changes: 115 additions & 0 deletions content/Coalesce.Starter.Vue.Data.Test/Utilities/TestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using IntelliTect.Coalesce;
using IntelliTect.Coalesce.TypeDefinition;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Moq;
using Moq.AutoMock;
using System.Security.Claims;

namespace Coalesce.Starter.Vue.Data.Test;

public class TestBase : IDisposable
{
protected SqliteDatabaseFixture DbFixture { get; }

private MockerScope? _CurrentMocker;

protected AutoMocker Mocker => _CurrentMocker ?? throw new InvalidOperationException("The current mocker has been disposed.");

protected AppDbContext Db => Mocker.Get<AppDbContext>();

protected ClaimsPrincipal CurrentUser { get; set; } = new();

public TestBase()
{
ReflectionRepository.Global.AddAssembly<AppDbContext>();

DbFixture = new SqliteDatabaseFixture();

_CurrentMocker = BeginMockScope(standalone: false);
}

public class MockerScope : AutoMocker, IDisposable
{
private readonly TestBase? _Parent;

public MockerScope(TestBase? parent) : base(MockBehavior.Loose)
{
_Parent = parent;
if (parent != null) parent._CurrentMocker = this;
}

public void Dispose()
{
if (_Parent == null) return;
Interlocked.CompareExchange(ref _Parent._CurrentMocker, null, this);
this.AsDisposable().Dispose();
}
}

/// <summary>
/// <para>
/// Create a new <see cref="Mocker"/>, allowing for new instances of all services
/// to be obtained - especially a new <see cref="AppDbContext"/>.
/// Persistence mechanisms are maintained, including the same <see cref="SqliteDatabaseFixture"/>.
/// </para>
/// <para>
/// Doing this allows you to verify that test setup steps haven't polluted the state
/// of services in such a way that will cause the test to behave differently than it would
/// in a real scenario.
/// </para>
/// <para>
/// For example, you should call this after setting up test data, and also in multi-step tests
/// where the steps would normally be performed by separate web requests/background jobs/etc.
/// </para>
/// </summary>
protected void RefreshServices()
{
_CurrentMocker?.Dispose();
BeginMockScope(standalone: false);
}

/// <summary>
/// Create an standalone mocker instance that can be used to acquire services.
/// Usually you should use the current mock scope in <see cref="Mocker"/>,
/// resetting it with <see cref="RefreshServices"/> as needed.
/// Only create an independent scope for operations like parallel processing
/// (an unusual thing to be doing in unit tests).
/// </summary>
protected MockerScope BeginMockScope() => BeginMockScope(true);

private MockerScope BeginMockScope(bool standalone = false)
{
var mocker = new MockerScope(standalone ? null : this);
var db = new AppDbContextForSqlite(DbFixture.Options);
mocker.Use(DbFixture.Options);
mocker.Use<AppDbContext>(db);

mocker.Use<CrudContext<AppDbContext>>(new CrudContext<AppDbContext>(
db,
() => CurrentUser
));

mocker.GetMock<IDbContextFactory<AppDbContext>>()
.Setup(x => x.CreateDbContextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(() => new AppDbContextForSqlite(DbFixture.Options));

mocker.GetMock<IDbContextFactory<AppDbContext>>()
.Setup(x => x.CreateDbContext())
.Returns(() => new AppDbContextForSqlite(DbFixture.Options));

mocker.Use<IMemoryCache>(new MemoryCache(new MemoryCacheOptions()));

// Register additional services required by tests,
// preferring real implementations where possible
// in order to improve test fidelity.

return mocker;
}

public virtual void Dispose()
{
_CurrentMocker?.Dispose();
DbFixture.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="IntelliTect.Coalesce" />
<PackageReference Include="IntelliTect.Coalesce" Version="$(CoalesceVersion)" />
</ItemGroup>
</Project>
1 change: 0 additions & 1 deletion content/Coalesce.Starter.Vue.Data/Models/Widget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@ public enum WidgetCategory
Whizbangs,
Sprecklesprockets,
Discombobulators,

}
1 change: 0 additions & 1 deletion content/Coalesce.Starter.Vue.Web/.vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"esbenp.prettier-vscode",
"antfu.goto-alias"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="IntelliTect.Coalesce.Vue" />
<PackageReference Include="IntelliTect.Coalesce.Vue" Version="$(CoalesceVersion)" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 1946205

Please sign in to comment.