From 19462050a4c78daafcf62bf02aece74f619a2f43 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Wed, 15 May 2024 14:31:19 -0700 Subject: [PATCH] feat: add a sample test project --- content/.gitignore | 3 + content/.vscode/extensions.json | 1 - .../Coalesce.Starter.Vue.Data.Test.csproj | 35 ++++++ .../UnitTest1.cs | 27 ++++ .../Utilities/AppDbContextForSqlite.cs | 25 ++++ .../Utilities/AssertionExtensions.cs | 72 +++++++++++ .../Utilities/SqliteDatabaseFixture.cs | 52 ++++++++ .../Utilities/TestBase.cs | 115 ++++++++++++++++++ .../Coalesce.Starter.Vue.Data.csproj | 7 +- .../Models/Widget.cs | 1 - .../.vscode/extensions.json | 1 - .../Coalesce.Starter.Vue.Web.csproj | 2 +- .../src/components/HelloWorld.vue | 14 +-- content/Coalesce.Starter.Vue.sln | 42 ++++--- content/Directory.Build.props | 2 + content/Directory.Packages.props | 15 --- 16 files changed, 364 insertions(+), 50 deletions(-) create mode 100644 content/Coalesce.Starter.Vue.Data.Test/Coalesce.Starter.Vue.Data.Test.csproj create mode 100644 content/Coalesce.Starter.Vue.Data.Test/UnitTest1.cs create mode 100644 content/Coalesce.Starter.Vue.Data.Test/Utilities/AppDbContextForSqlite.cs create mode 100644 content/Coalesce.Starter.Vue.Data.Test/Utilities/AssertionExtensions.cs create mode 100644 content/Coalesce.Starter.Vue.Data.Test/Utilities/SqliteDatabaseFixture.cs create mode 100644 content/Coalesce.Starter.Vue.Data.Test/Utilities/TestBase.cs delete mode 100644 content/Directory.Packages.props diff --git a/content/.gitignore b/content/.gitignore index 8361e8e..aef0b94 100644 --- a/content/.gitignore +++ b/content/.gitignore @@ -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 diff --git a/content/.vscode/extensions.json b/content/.vscode/extensions.json index 9505d30..3c500cb 100644 --- a/content/.vscode/extensions.json +++ b/content/.vscode/extensions.json @@ -7,7 +7,6 @@ "streetsidesoftware.code-spell-checker", "dbaeumer.vscode-eslint", "vue.volar", - "vue.vscode-typescript-vue-plugin", "esbenp.prettier-vscode", "antfu.goto-alias" ], diff --git a/content/Coalesce.Starter.Vue.Data.Test/Coalesce.Starter.Vue.Data.Test.csproj b/content/Coalesce.Starter.Vue.Data.Test/Coalesce.Starter.Vue.Data.Test.csproj new file mode 100644 index 0000000..e253b59 --- /dev/null +++ b/content/Coalesce.Starter.Vue.Data.Test/Coalesce.Starter.Vue.Data.Test.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/content/Coalesce.Starter.Vue.Data.Test/UnitTest1.cs b/content/Coalesce.Starter.Vue.Data.Test/UnitTest1.cs new file mode 100644 index 0000000..d8128f0 --- /dev/null +++ b/content/Coalesce.Starter.Vue.Data.Test/UnitTest1.cs @@ -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); + + + } +} \ No newline at end of file diff --git a/content/Coalesce.Starter.Vue.Data.Test/Utilities/AppDbContextForSqlite.cs b/content/Coalesce.Starter.Vue.Data.Test/Utilities/AppDbContextForSqlite.cs new file mode 100644 index 0000000..729ddaa --- /dev/null +++ b/content/Coalesce.Starter.Vue.Data.Test/Utilities/AppDbContextForSqlite.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Coalesce.Starter.Vue.Data.Test; + +public class AppDbContextForSqlite(DbContextOptions 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().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + } + + public class DateOnlyToDateTimeConverter : ValueConverter + { + public DateOnlyToDateTimeConverter() + : base( + dateOnly => dateOnly.ToDateTime(new TimeOnly()), + dateTime => DateOnly.FromDateTime(dateTime) + ) + { } + } +} diff --git a/content/Coalesce.Starter.Vue.Data.Test/Utilities/AssertionExtensions.cs b/content/Coalesce.Starter.Vue.Data.Test/Utilities/AssertionExtensions.cs new file mode 100644 index 0000000..c789a64 --- /dev/null +++ b/content/Coalesce.Starter.Vue.Data.Test/Utilities/AssertionExtensions.cs @@ -0,0 +1,72 @@ +using IntelliTect.Coalesce.Models; + +namespace Coalesce.Starter.Vue.Data.Test; + +public static class AssertionExtensions +{ + /// + /// Asserts that the result was a failure. + /// + public static void AssertError(this ApiResult result) + { + Assert.False(result.WasSuccessful); + } + + /// + /// Asserts that the result was a failure. + /// + /// Expected error message. + public static void AssertError(this ApiResult result, string message) + { + result.AssertError(); + Assert.Equal(message, result.Message); + } + + /// + /// Asserts that the result was successful. + /// + 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); + } + + /// + /// Asserts that the result was successful. + /// + public static T AssertSuccess(this ItemResult result) + { + Assert.Null(result.Message); + Assert.True(result.WasSuccessful); + return result.Object ?? throw new ArgumentException("Sucessful result unexpectedly returned null object"); + } + + /// + /// Asserts that the result was successful. + /// + public static async Task AssertSuccess(this Task> resultTask) + { + var result = await resultTask; + return result.AssertSuccess(); + } + + /// + /// Asserts that the result was successful. + /// + public static async Task AssertSuccess(this Task resultTask) + { + var result = await resultTask; + Assert.True(result.WasSuccessful); + } + + /// + /// Asserts that the result was successful. + /// + /// Expected value on the result. + public static void AssertSuccess(this ItemResult result, T expectedValue) + { + result.AssertSuccess(); + Assert.Equal(expectedValue, result.Object); + } +} diff --git a/content/Coalesce.Starter.Vue.Data.Test/Utilities/SqliteDatabaseFixture.cs b/content/Coalesce.Starter.Vue.Data.Test/Utilities/SqliteDatabaseFixture.cs new file mode 100644 index 0000000..4a661fe --- /dev/null +++ b/content/Coalesce.Starter.Vue.Data.Test/Utilities/SqliteDatabaseFixture.cs @@ -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 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(); + 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(); + } +} diff --git a/content/Coalesce.Starter.Vue.Data.Test/Utilities/TestBase.cs b/content/Coalesce.Starter.Vue.Data.Test/Utilities/TestBase.cs new file mode 100644 index 0000000..4f19995 --- /dev/null +++ b/content/Coalesce.Starter.Vue.Data.Test/Utilities/TestBase.cs @@ -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(); + + protected ClaimsPrincipal CurrentUser { get; set; } = new(); + + public TestBase() + { + ReflectionRepository.Global.AddAssembly(); + + 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(); + } + } + + /// + /// + /// Create a new , allowing for new instances of all services + /// to be obtained - especially a new . + /// Persistence mechanisms are maintained, including the same . + /// + /// + /// 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. + /// + /// + /// 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. + /// + /// + protected void RefreshServices() + { + _CurrentMocker?.Dispose(); + BeginMockScope(standalone: false); + } + + /// + /// Create an standalone mocker instance that can be used to acquire services. + /// Usually you should use the current mock scope in , + /// resetting it with as needed. + /// Only create an independent scope for operations like parallel processing + /// (an unusual thing to be doing in unit tests). + /// + 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(db); + + mocker.Use>(new CrudContext( + db, + () => CurrentUser + )); + + mocker.GetMock>() + .Setup(x => x.CreateDbContextAsync(It.IsAny())) + .ReturnsAsync(() => new AppDbContextForSqlite(DbFixture.Options)); + + mocker.GetMock>() + .Setup(x => x.CreateDbContext()) + .Returns(() => new AppDbContextForSqlite(DbFixture.Options)); + + mocker.Use(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(); + } +} diff --git a/content/Coalesce.Starter.Vue.Data/Coalesce.Starter.Vue.Data.csproj b/content/Coalesce.Starter.Vue.Data/Coalesce.Starter.Vue.Data.csproj index e44f2cd..470b983 100644 --- a/content/Coalesce.Starter.Vue.Data/Coalesce.Starter.Vue.Data.csproj +++ b/content/Coalesce.Starter.Vue.Data/Coalesce.Starter.Vue.Data.csproj @@ -4,12 +4,11 @@ - - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/content/Coalesce.Starter.Vue.Data/Models/Widget.cs b/content/Coalesce.Starter.Vue.Data/Models/Widget.cs index ee24fd2..8be167f 100644 --- a/content/Coalesce.Starter.Vue.Data/Models/Widget.cs +++ b/content/Coalesce.Starter.Vue.Data/Models/Widget.cs @@ -16,5 +16,4 @@ public enum WidgetCategory Whizbangs, Sprecklesprockets, Discombobulators, - } diff --git a/content/Coalesce.Starter.Vue.Web/.vscode/extensions.json b/content/Coalesce.Starter.Vue.Web/.vscode/extensions.json index 9505d30..3c500cb 100644 --- a/content/Coalesce.Starter.Vue.Web/.vscode/extensions.json +++ b/content/Coalesce.Starter.Vue.Web/.vscode/extensions.json @@ -7,7 +7,6 @@ "streetsidesoftware.code-spell-checker", "dbaeumer.vscode-eslint", "vue.volar", - "vue.vscode-typescript-vue-plugin", "esbenp.prettier-vscode", "antfu.goto-alias" ], diff --git a/content/Coalesce.Starter.Vue.Web/Coalesce.Starter.Vue.Web.csproj b/content/Coalesce.Starter.Vue.Web/Coalesce.Starter.Vue.Web.csproj index 440012d..ff687da 100644 --- a/content/Coalesce.Starter.Vue.Web/Coalesce.Starter.Vue.Web.csproj +++ b/content/Coalesce.Starter.Vue.Web/Coalesce.Starter.Vue.Web.csproj @@ -9,7 +9,7 @@ - + diff --git a/content/Coalesce.Starter.Vue.Web/src/components/HelloWorld.vue b/content/Coalesce.Starter.Vue.Web/src/components/HelloWorld.vue index 672df74..b956b2e 100644 --- a/content/Coalesce.Starter.Vue.Web/src/components/HelloWorld.vue +++ b/content/Coalesce.Starter.Vue.Web/src/components/HelloWorld.vue @@ -128,21 +128,9 @@ - Vue Language Features (Volar) + Vue - Official -
  • - - TypeScript Vue Plugin (Volar) - - (alternatively, use Volar - takeover mode) -
  • true true 4.9 + + 4.0.0-alpha.20240502.1 diff --git a/content/Directory.Packages.props b/content/Directory.Packages.props deleted file mode 100644 index 974f891..0000000 --- a/content/Directory.Packages.props +++ /dev/null @@ -1,15 +0,0 @@ - - - true - false - - 4.0.0-alpha.20231221.1 - - - - - - - - -