diff --git a/Fonlow.Testing.Basic/Fonlow.Testing.Basic.csproj b/Fonlow.Testing.Basic/Fonlow.Testing.Basic.csproj index b5aa8f1..d0715cf 100644 --- a/Fonlow.Testing.Basic/Fonlow.Testing.Basic.csproj +++ b/Fonlow.Testing.Basic/Fonlow.Testing.Basic.csproj @@ -2,7 +2,7 @@ net8.0 - 1.0 + 1.1 Zijian Huang Basic types and functions of Fonlow.Testing.Integration suite. diff --git a/Fonlow.Testing.Basic/TestingSettings.cs b/Fonlow.Testing.Basic/TestingSettings.cs index bd1b302..7ef1def 100644 --- a/Fonlow.Testing.Basic/TestingSettings.cs +++ b/Fonlow.Testing.Basic/TestingSettings.cs @@ -4,175 +4,200 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; namespace Fonlow.Testing { - /// - /// Data model of the "Testing" section "appsettings.json" and optionally "appsettings.BuildConfiguration.json"; - /// and the singleton object to access the settings read from the JSON files - /// - public sealed class TestingSettings - { - private TestingSettings() - { - } - - private static readonly Lazy lazy = - new Lazy(() => Create()); - - public static TestingSettings Instance { get { return lazy.Value; } } - - static TestingSettings Create() - { - var obj = new TestingSettings(); - IConfigurationBuilder configBuilder = new ConfigurationBuilder().AddJsonFile("appsettings.json"); - obj.BuildConfiguration = GetBuildConfiguration(); - - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - obj.ExecutableExt = ".exe"; - } - - string specificAppSettingsFilename = $"appsettings.{obj.BuildConfiguration}.json"; - if (Path.Exists(specificAppSettingsFilename)) - { - configBuilder.AddJsonFile(specificAppSettingsFilename); - } - - var config = configBuilder.Build(); - config.Bind("Testing", obj);//work only when properties are not with private setter. - - return obj; - } - - static string GetBuildConfiguration() - { - var executingAssembly = Assembly.GetExecutingAssembly(); - var location = executingAssembly.Location; - var dir = Path.GetDirectoryName(location); - var pathNames = Directory.GetFiles(dir, "*.dll"); - List testAssemblies = new List(); - foreach (var p in pathNames) - { - try - { - var a = Assembly.LoadFile(p); - var referencedAssemblies = a.GetReferencedAssemblies(); - if (referencedAssemblies.Any(d => d.Name == "xunit.core") && !Path.GetFileName(p).StartsWith("xunit.", StringComparison.OrdinalIgnoreCase)) - { - testAssemblies.Add(a); - } - } - catch (FileLoadException) - { - continue; - } - catch (BadImageFormatException) - { - continue; - } - } - - if (testAssemblies.Count > 0) - { - var assemblyConfigurationAttribute = testAssemblies[0].GetCustomAttribute(); - return assemblyConfigurationAttribute?.Configuration; - } - else - { - return "Debug"; - } - } - - public ServiceCommand[] ServiceCommands { get; set; } - - public CopyItem[] CopyItems { get; set; } - - /// - /// Used when Web resource is there, no need to be under the control of the test suite. - /// - public string BaseUrl { get; set; } - - public string Username { get; set; } - public string Password { get; set; } - - /// - /// For testing with many different user credentials. - /// - public UsernamePassword[] Users { get; set; } - - /// - /// Build configuration of the test suite such as Debug, Release or whatever custom build configuration. - /// ServiceCommandFixture will replace {BuildConfiguration} in commandPath and arguments with this. - /// - [System.Text.Json.Serialization.JsonIgnore] - public string BuildConfiguration { get; private set; } - - /// - /// The extention name of executable file. On Win, .exe, and on Linux and MacOs, empty. - /// - [System.Text.Json.Serialization.JsonIgnore] - public string ExecutableExt { get; private set; } = string.Empty; - - [Obsolete("In favor of ServiceCommandFixture")] - public string DotNetServiceAssemblyPath { get; set; } - - /// - /// For IIS Express, host site name - /// - [Obsolete("In favor of ServiceCommandFixture")] - public string HostSite { get; set; } - - /// - /// For IIS Express, application pool - /// - [Obsolete("In favor of ServiceCommandFixture")] - public string HostSiteApplicationPool { get; set; } - - /// - /// For IIS Express, the lib needs to be aware the SLN root - /// - [Obsolete("In favor of ServiceCommandFixture")] - public string SlnRoot { get; set; } - - /// - /// For IIS Express, the lib needs to know the SLN name - /// - [Obsolete("In favor of ServiceCommandFixture")] - public string SlnName { get; set; } - } - - public sealed class UsernamePassword - { - public string Username { get; set; } - public string Password { get; set; } - } - - public sealed class ServiceCommand - { - public string CommandPath { get; set; } - - public bool IsPowerShellCommand { get; set; } - - public string Arguments { get; set; } - - /// - /// Some services may take some seconds to launch then listen, especially in GitHub Actions which VM/container could be slow. A good bet may be 5 seconds. - /// - public int Delay { get; set; } - public string ConnectionString { get; set; } - public string BaseUrl { get; set; } - - /// - /// For testing with many different user credentials with different authorization. - /// - /// Obviously 2FA and alike are not welcome. Good enough for integration tests, but not E2E. - public UsernamePassword[] Users { get; set; } - } - - public sealed class CopyItem - { - public string Source { get; set; } - public string Destination { get; set; } - } + /// + /// Data model of the "Testing" section "appsettings.json" and optionally "appsettings.BuildConfiguration.json"; + /// and the singleton object to access the settings read from the JSON files + /// + public sealed class TestingSettings + { + #region Singleton and init + private TestingSettings() + { + } + + private static readonly Lazy lazy = + new Lazy(() => Create()); + + public static TestingSettings Instance { get { return lazy.Value; } } + + static TestingSettings Create() + { + var obj = new TestingSettings(); + IConfigurationBuilder configBuilder = new ConfigurationBuilder().AddJsonFile("appsettings.json"); + obj.BuildConfiguration = GetBuildConfiguration(); + obj.RuntimeId = RuntimeInformation.RuntimeIdentifier; + + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + obj.ExecutableExt = ".exe"; + } + + string specificAppSettingsFilename = $"appsettings.{obj.BuildConfiguration}.json"; + if (Path.Exists(specificAppSettingsFilename)) + { + configBuilder.AddJsonFile(specificAppSettingsFilename); + } + + var config = configBuilder.Build(); + config.Bind("Testing", obj);//work only when properties are not with private setter. + + return obj; + } + + static string GetBuildConfiguration() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + var location = executingAssembly.Location; + var dir = Path.GetDirectoryName(location); + var pathNames = Directory.GetFiles(dir, "*.dll"); + List testAssemblies = new List(); + foreach (var p in pathNames) + { + try + { + var a = Assembly.LoadFile(p); + var referencedAssemblies = a.GetReferencedAssemblies(); + if (referencedAssemblies.Any(d => d.Name == "xunit.core") && !Path.GetFileName(p).StartsWith("xunit.", StringComparison.OrdinalIgnoreCase)) + { + testAssemblies.Add(a); + } + } + catch (FileLoadException) + { + continue; + } + catch (BadImageFormatException) + { + continue; + } + } + + if (testAssemblies.Count > 0) + { + var assemblyConfigurationAttribute = testAssemblies[0].GetCustomAttribute(); + return assemblyConfigurationAttribute?.Configuration; + } + else + { + return "Debug"; + } + } + #endregion + + #region Settings in appsettings.json + /// + /// Service Commands are executed in the order of JSON data initialization. + /// + public IReadOnlyDictionary ServiceCommands { get; set; } + + /// + /// Each CopyItem is executed synchronous, so items are executed subsequently. + /// + public CopyItem[] CopyItems { get; set; } + + /// + /// Used when Web resource is there, no need to be under the control of the test suite. + /// + public string BaseUrl { get; set; } + + public string Username { get; set; } + public string Password { get; set; } + + /// + /// For testing with many different user credentials. + /// + public UsernamePassword[] Users { get; set; } + + #endregion + + + #region Environment variables + /// + /// Build configuration of the test suite such as Debug, Release or whatever custom build configuration. + /// ServiceCommandFixture will replace {BuildConfiguration} in commandPath and arguments with this. + /// + [System.Text.Json.Serialization.JsonIgnore] + public string BuildConfiguration { get; private set; } + + /// + /// RuntimeInformation.RuntimeIdentifier such as win-x64, linux-x64. Generally used for publishing to a specific platform. + /// + [System.Text.Json.Serialization.JsonIgnore] + public string RuntimeId { get; private set; } + + /// + /// The extention name of executable file. On Win, it is ".exe" including the dot, and on Linux and MacOs, empty. + /// + [System.Text.Json.Serialization.JsonIgnore] + public string ExecutableExt { get; private set; } = string.Empty; + #endregion + + #region Obsolete + [Obsolete("In favor of ServiceCommandFixture")] + public string DotNetServiceAssemblyPath { get; set; } + + /// + /// For IIS Express, host site name + /// + [Obsolete("In favor of ServiceCommandFixture")] + public string HostSite { get; set; } + + /// + /// For IIS Express, application pool + /// + [Obsolete("In favor of ServiceCommandFixture")] + public string HostSiteApplicationPool { get; set; } + + /// + /// For IIS Express, the lib needs to be aware the SLN root + /// + [Obsolete("In favor of ServiceCommandFixture")] + public string SlnRoot { get; set; } + + /// + /// For IIS Express, the lib needs to know the SLN name + /// + [Obsolete("In favor of ServiceCommandFixture")] + public string SlnName { get; set; } + #endregion + + } + + public sealed class UsernamePassword + { + public string Username { get; set; } + public string Password { get; set; } + } + + public sealed class ServiceCommand + { + public string CommandPath { get; set; } + + public bool IsPowerShellCommand { get; set; } + + public string Arguments { get; set; } + + /// + /// Some services may take some seconds to launch then listen, especially in GitHub Actions which VM/container could be slow. A good bet may be 5 seconds. + /// + public int Delay { get; set; } + public string ConnectionString { get; set; } + public string BaseUrl { get; set; } + + /// + /// For testing with many different user credentials with different authorization. + /// + /// Obviously 2FA and alike are not welcome. Good enough for integration tests, but not E2E. + public UsernamePassword[] Users { get; set; } + } + + public sealed class CopyItem + { + public string Source { get; set; } + public string Destination { get; set; } + } } diff --git a/Fonlow.Testing.HttpCore/Fonlow.Testing.HttpCore.csproj b/Fonlow.Testing.HttpCore/Fonlow.Testing.HttpCore.csproj index 1b51f64..3ba9059 100644 --- a/Fonlow.Testing.HttpCore/Fonlow.Testing.HttpCore.csproj +++ b/Fonlow.Testing.HttpCore/Fonlow.Testing.HttpCore.csproj @@ -10,7 +10,7 @@ https://github.com/zijianhuang/FonlowTesting xunit nunit mstest unittesting iisexpress iis webapi restful dotnet en - 3.5 + 3.5.1 Upgraded to .NET 8. README.md latest-all diff --git a/Fonlow.Testing.Integration/Fonlow.Testing.Integration.csproj b/Fonlow.Testing.Integration/Fonlow.Testing.Integration.csproj index b636290..02d1b37 100644 --- a/Fonlow.Testing.Integration/Fonlow.Testing.Integration.csproj +++ b/Fonlow.Testing.Integration/Fonlow.Testing.Integration.csproj @@ -2,7 +2,7 @@ net8.0 - 1.0 + 1.1 Zijian Huang Utilities and fixtures for integration testing with xUnit diff --git a/Fonlow.Testing.ServiceCore/Fonlow.Testing.ServiceCore.csproj b/Fonlow.Testing.ServiceCore/Fonlow.Testing.ServiceCore.csproj index 2ece4c3..3fec8a0 100644 --- a/Fonlow.Testing.ServiceCore/Fonlow.Testing.ServiceCore.csproj +++ b/Fonlow.Testing.ServiceCore/Fonlow.Testing.ServiceCore.csproj @@ -11,7 +11,7 @@ xunit nunit mstest unittesting iisexpress iis dotnet en true - 3.7 + 3.8 README.md latest-all diff --git a/Fonlow.Testing.ServiceCore/ServiceCommandsFixture.cs b/Fonlow.Testing.ServiceCore/ServiceCommandsFixture.cs index a3b8523..1aa9aec 100644 --- a/Fonlow.Testing.ServiceCore/ServiceCommandsFixture.cs +++ b/Fonlow.Testing.ServiceCore/ServiceCommandsFixture.cs @@ -3,69 +3,75 @@ namespace Fonlow.Testing { - /// - /// Launch services defined in TestingSettings - /// - public class ServiceCommandsFixture : IDisposable - { - /// - /// Create the fixture. And this constructor is also used in XUnit.ICollectionFixture. - /// - public ServiceCommandsFixture() - { - if (TestingSettings.Instance.ServiceCommands != null) - { - foreach (var item in TestingSettings.Instance.ServiceCommands) - { - item.Arguments = item.Arguments?.Replace("{BuildConfiguration}", TestingSettings.Instance.BuildConfiguration); - item.CommandPath = item.CommandPath?.Replace("{BuildConfiguration}", TestingSettings.Instance.BuildConfiguration); - item.CommandPath = item.CommandPath?.Replace("{ExecutableExt}", TestingSettings.Instance.ExecutableExt); - var serviceCommandAgent = new ServiceCommandAgent(item); - serviceCommandAgent.Start(); - serviceCommandAgents.Add(serviceCommandAgent); - } - } + /// + /// Launch services defined in TestingSettings + /// + public class ServiceCommandsFixture : IDisposable + { + /// + /// Create the fixture. And this constructor is also used in XUnit.ICollectionFixture. + /// + public ServiceCommandsFixture() + { + if (TestingSettings.Instance.ServiceCommands != null) + { + foreach (var key in TestingSettings.Instance.ServiceCommands.Keys) + { + var item = TestingSettings.Instance.ServiceCommands[key]; + item.Arguments = item.Arguments?.Replace("{BuildConfiguration}", TestingSettings.Instance.BuildConfiguration) + .Replace("{ExecutableExt}", TestingSettings.Instance.ExecutableExt) + .Replace("{RuntimeId}", TestingSettings.Instance.RuntimeId); + item.CommandPath = item.CommandPath?.Replace("{BuildConfiguration}", TestingSettings.Instance.BuildConfiguration) + .Replace("{ExecutableExt}", TestingSettings.Instance.ExecutableExt) + .Replace("{RuntimeId}", TestingSettings.Instance.RuntimeId); + var serviceCommandAgent = new ServiceCommandAgent(item); + serviceCommandAgent.Start(); + serviceCommandAgents.Add(serviceCommandAgent); + } + } - if (TestingSettings.Instance.CopyItems != null) - { - foreach (var item in TestingSettings.Instance.CopyItems) - { - item.Source = item.Source?.Replace("{BuildConfiguration}", TestingSettings.Instance.BuildConfiguration); - item.Destination = item.Destination?.Replace("{BuildConfiguration}", TestingSettings.Instance.BuildConfiguration); - DeploymentItemFixture.CopyDirectory(item.Source, item.Destination, true); - } - } - } + if (TestingSettings.Instance.CopyItems != null) + { + foreach (var item in TestingSettings.Instance.CopyItems) + { + item.Source = item.Source?.Replace("{BuildConfiguration}", TestingSettings.Instance.BuildConfiguration) + .Replace("{RuntimeId}", TestingSettings.Instance.RuntimeId); + item.Destination = item.Destination?.Replace("{BuildConfiguration}", TestingSettings.Instance.BuildConfiguration) + .Replace("{RuntimeId}", TestingSettings.Instance.RuntimeId); + DeploymentItemFixture.CopyDirectory(item.Source, item.Destination, true); + } + } + } - List serviceCommandAgents = new List(); + List serviceCommandAgents = new List(); - bool disposed; + bool disposed; - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposing) - { - if (!disposed) - { - if (disposing) - { - foreach (var agent in serviceCommandAgents) - { - if (agent != null) - { - agent.Stop(); - } - } - } + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + foreach (var agent in serviceCommandAgents) + { + if (agent != null) + { + agent.Stop(); + } + } + } - disposed = true; - } - } + disposed = true; + } + } - } + } } \ No newline at end of file diff --git a/Fonlow.Testing.Utilities/Fonlow.Testing.Utilities.csproj b/Fonlow.Testing.Utilities/Fonlow.Testing.Utilities.csproj index 920a759..1fdc0a7 100644 --- a/Fonlow.Testing.Utilities/Fonlow.Testing.Utilities.csproj +++ b/Fonlow.Testing.Utilities/Fonlow.Testing.Utilities.csproj @@ -2,7 +2,7 @@ net8.0 - 2.0 + 2.0.1 Zijian Huang Basic types and functions of Fonlow.Testing.Integration suite. diff --git a/README.md b/README.md index 26d27a0..e8afee6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # Fonlow Testing -The goal of this component suite is to help .NET application developers to run integration tests with minimum fixtures in codes and in CI environments being hosted either in local dev machine and the team CI/CD server. You as a software developer will be able to: +The goal of this component suite is to help .NET application developers to run integration tests with minimum fixtures in codes and in CI environments being hosted either in local dev machine and the team CI/CD server on Windows, Linux or MacOs. + +The overall design cover such scenarios: +* Your team includes developers whose dev machines are running Windows, MacOS and Linux. +* You have a team CI/CD environment on GitHub Actions/Workflows, TeamCity and Azure DevOps etc. +* You want a new clone/pull from the revision control system like Git or GitHub can build, run and execute most if not all integrations right away, regardless of the OS of each dev PC. + +You as a software developer will be able to: 1. Run integration tests as early, frequent and quickly as possible on a local dev machine, thus this may minimize the chances of merge conflicts in codes or in the logics of system integration. 2. Most of the fixtures and configuration having been working on a local dev machine should be working in the team CI/CT server, Windows based or Linux based. Therefore, this will reduce the costs of the setup and the maintenance of the CI/CD server. @@ -34,20 +41,20 @@ appsettings.json of the integration test suite: ```json { "Testing": { - "ServiceCommands": [ - { - "CommandPath": "../../../../../PoemsMyDbCreator/bin/{BuildConfiguration}/net8.0/PoemsMyDbCreator.exe", - "Arguments": "Fonlow.EntityFrameworkCore.MySql \"server=localhost;port=3306;Uid=root; password=zzzzzzzz; database=Poems_Test; Persist Security Info=True;Allow User Variables=true\"", + "ServiceCommands": { + "ResetDb": { + "CommandPath": "../../../../../PoemsMyDbCreator/bin/{BuildConfiguration}/net8.0/PoemsMyDbCreator{ExecutableExt}", + "Arguments": "Fonlow.EntityFrameworkCore.MySql \"server=localhost;port=3306;Uid=root; password=Securd321*; database=Poems_Test; Persist Security Info=True;Allow User Variables=true\"", "Delay": 0 }, - { + "LaunchWebApi": { "CommandPath": "dotnet", "Arguments": "run --project ../../../../../PoetryApp/PoetryApp.csproj --no-build --configuration {BuildConfiguration}", "BaseUrl": "http://localhost:5300/", "Delay": 1 } - ], + }, "Username": "admin", "Password": "MyPassword123" @@ -85,7 +92,7 @@ namespace PoemsIntegrationTests { } - public AuthHttpClientWithUsername(HttpMessageHandler handler) : base(new Uri(TestingSettings.Instance.ServiceCommands[1].BaseUrl), TestingSettings.Instance.Username, TestingSettings.Instance.Password, handler) + public AuthHttpClientWithUsername(HttpMessageHandler handler) : base(new Uri(TestingSettings.Instance.ServiceCommands["LaunchWebApi"].BaseUrl), TestingSettings.Instance.Username, TestingSettings.Instance.Password, handler) { } } @@ -147,8 +154,8 @@ The first Web API is for OAuth2 authentication, and the Pet Store Web API recogn "Destination": "../../../../../Core3WebApi/bin/{BuildConfiguration}/net8.0/DemoApp_Data" } ], - "ServiceCommands": [ - { + "ServiceCommands": { + "LaunchIdentityWebApi": { "CommandPath": "../../../../../Core3WebApi/bin/{BuildConfiguration}/net8.0/Core3WebApi{ExecutableExt}", "BaseUrl": "http://127.0.0.1:5000/", "Delay": 1, @@ -160,7 +167,7 @@ The first Web API is for OAuth2 authentication, and the Pet Store Web API recogn ] }, - { + "LaunchPetWebApi": { "CommandPath": "../../../../../PetWebApi/bin/{BuildConfiguration}/net8.0/PetWebApi{ExecutableExt}", "BaseUrl": "http://127.0.0.1:6000/", "Delay": 5, @@ -171,7 +178,7 @@ The first Web API is for OAuth2 authentication, and the Pet Store Web API recogn } ] } - ] + } }, } @@ -240,14 +247,14 @@ appsettings.Debug.json: ```json { "Testing": { - "ServiceCommands": [ - { + "ServiceCommands": { + "LaunchDemoCoreWeb": { "CommandPath": "dotnet", "Arguments": "run --project ../../../../../DemoCoreWeb/DemoCoreWeb.csproj --no-build --configuration Debug", "BaseUrl": "http://127.0.0.1:5000/", "Delay": 2 } - ] + } } } ``` @@ -255,11 +262,18 @@ appsettings.Debug.json: ## Settings +The settings of "Testing" in appsettings.json are mapped to the following class: ```csharp public sealed class TestingSettings { - public ServiceCommand[] ServiceCommands { get; set; } + /// + /// Service Commands are executed in the order of JSON data initialization. + /// + public IReadOnlyDictionary ServiceCommands { get; set; } + /// + /// Each CopyItem is executed synchronous, so items are executed subsequently. + /// public CopyItem[] CopyItems { get; set; } /// @@ -296,6 +310,12 @@ public sealed class ServiceCommand public int Delay { get; set; } public string ConnectionString { get; set; } public string BaseUrl { get; set; } + + /// + /// For testing with many different user credentials with different authorization. + /// + /// Obviously 2FA and alike are not welcome. Good enough for integration tests, but not E2E. + public UsernamePassword[] Users { get; set; } } ``` @@ -355,6 +375,7 @@ Examples: * https://github.com/zijianhuang/webapiclientgen/actions * https://github.com/zijianhuang/openapiclientgen/actions +* https://github.com/zijianhuang/AuthEF/actions # Alternatives diff --git a/Tests/ServiceCommandTests/WeatherIntegrationTests.cs b/Tests/ServiceCommandTests/WeatherIntegrationTests.cs index 7f02263..58d99f4 100644 --- a/Tests/ServiceCommandTests/WeatherIntegrationTests.cs +++ b/Tests/ServiceCommandTests/WeatherIntegrationTests.cs @@ -8,7 +8,7 @@ public class WhetherApiFixture : BasicHttpClient { public WhetherApiFixture() { - var c = TestingSettings.Instance.ServiceCommands[1]; + var c = TestingSettings.Instance.ServiceCommands["LaunchWebApi"]; this.HttpClient.BaseAddress = new System.Uri(c.BaseUrl); Api = new DemoCoreWeb.Controllers.Client.WeatherForecast(this.HttpClient); } diff --git a/Tests/ServiceCommandTests/appsettings.json b/Tests/ServiceCommandTests/appsettings.json index c116bbd..eb21455 100644 --- a/Tests/ServiceCommandTests/appsettings.json +++ b/Tests/ServiceCommandTests/appsettings.json @@ -1,16 +1,16 @@ { "Testing": { - "ServiceCommands": [ - { + "ServiceCommands": { + "CopySomeFile": { "CommandPath": "Copy-Item ../../../PowerShellTests.cs ./D_PowerShellTests.cs", "IsPowerShellCommand": true }, - { + "LaunchWebApi":{ "CommandPath": "../../../../../DemoCoreWeb/bin/{BuildConfiguration}/net8.0/DemoCoreWeb{ExecutableExt}", "BaseUrl": "http://127.0.0.1:5000/", "Delay": 3 } - ], + }, "CopyItems": [ { "Source": "../../../Assets",