diff --git a/Logging.XUnit.sln b/Logging.XUnit.sln index 534abe05..8f780a41 100644 --- a/Logging.XUnit.sln +++ b/Logging.XUnit.sln @@ -52,6 +52,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MartinCostello.Logging.XUni EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MartinCostello.Logging.XUnit", "src\Logging.XUnit\MartinCostello.Logging.XUnit.csproj", "{BB443063-F523-474D-83E3-D5FF5B075950}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp", "tests\SampleApp\SampleApp.csproj", "{B690F271-3B5D-4975-A607-AED1768595B1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,6 +68,10 @@ Global {BB443063-F523-474D-83E3-D5FF5B075950}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB443063-F523-474D-83E3-D5FF5B075950}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB443063-F523-474D-83E3-D5FF5B075950}.Release|Any CPU.Build.0 = Release|Any CPU + {B690F271-3B5D-4975-A607-AED1768595B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B690F271-3B5D-4975-A607-AED1768595B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B690F271-3B5D-4975-A607-AED1768595B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B690F271-3B5D-4975-A607-AED1768595B1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -76,6 +82,7 @@ Global {F37263E7-6656-4A2B-B02E-538B5F9970BD} = {D0426D09-1FF8-4E1F-A9AF-38DDEE5D7CCA} {331BC69B-DFFC-4174-8A97-6335BF6D86CF} = {278BCCB1-39B2-46DB-9395-7F85995A6132} {BB443063-F523-474D-83E3-D5FF5B075950} = {2684B19D-7D49-4099-8BD2-4D281455EB29} + {B690F271-3B5D-4975-A607-AED1768595B1} = {278BCCB1-39B2-46DB-9395-7F85995A6132} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3B9E157C-5E92-4357-B233-281B4530EABD} diff --git a/src/Logging.XUnit/AmbientTestOutputHelperAccessor.cs b/src/Logging.XUnit/AmbientTestOutputHelperAccessor.cs new file mode 100644 index 00000000..d1b22b61 --- /dev/null +++ b/src/Logging.XUnit/AmbientTestOutputHelperAccessor.cs @@ -0,0 +1,31 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Threading; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// + /// A class representing an implementation of that + /// stores the as an asynchronous local value. This class cannot be inherited. + /// + internal sealed class AmbientTestOutputHelperAccessor : ITestOutputHelperAccessor + { + /// + /// A backing field for the for the current thread. + /// + private static readonly AsyncLocal _current = new AsyncLocal(); + +#pragma warning disable CA1822 + /// + /// Gets or sets the current . + /// + public ITestOutputHelper OutputHelper + { + get { return _current.Value; } + set { _current.Value = value; } + } +#pragma warning restore CA1822 + } +} diff --git a/src/Logging.XUnit/ITestOutputHelperAccessor.cs b/src/Logging.XUnit/ITestOutputHelperAccessor.cs new file mode 100644 index 00000000..f9a517b8 --- /dev/null +++ b/src/Logging.XUnit/ITestOutputHelperAccessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// + /// Defines a property for accessing an . + /// + public interface ITestOutputHelperAccessor + { + /// + /// Gets or sets the to use. + /// + ITestOutputHelper OutputHelper { get; set; } + } +} diff --git a/src/Logging.XUnit/TestOutputHelperAccessor.cs b/src/Logging.XUnit/TestOutputHelperAccessor.cs new file mode 100644 index 00000000..e4e6257c --- /dev/null +++ b/src/Logging.XUnit/TestOutputHelperAccessor.cs @@ -0,0 +1,31 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// + /// A class representing the default implementation of . This class cannot be inherited. + /// + internal sealed class TestOutputHelperAccessor : ITestOutputHelperAccessor + { + /// + /// Initializes a new instance of the class. + /// + /// The to use. + /// + /// is . + /// + internal TestOutputHelperAccessor(ITestOutputHelper outputHelper) + { + OutputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); + } + + /// + /// Gets or sets the current . + /// + public ITestOutputHelper OutputHelper { get; set; } + } +} diff --git a/src/Logging.XUnit/XUnitLogger.cs b/src/Logging.XUnit/XUnitLogger.cs index 435ffeb9..85ab8760 100644 --- a/src/Logging.XUnit/XUnitLogger.cs +++ b/src/Logging.XUnit/XUnitLogger.cs @@ -37,9 +37,9 @@ public class XUnitLogger : ILogger private static StringBuilder _logBuilder; /// - /// The to use. This field is read-only. + /// The to use. This field is read-only. /// - private readonly ITestOutputHelper _outputHelper; + private readonly ITestOutputHelperAccessor _accessor; /// /// Gets or sets the filter to use. @@ -56,9 +56,23 @@ public class XUnitLogger : ILogger /// or is . /// public XUnitLogger(string name, ITestOutputHelper outputHelper, XUnitLoggerOptions options) + : this(name, new TestOutputHelperAccessor(outputHelper), options) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name for messages produced by the logger. + /// The to use. + /// The to use. + /// + /// or is . + /// + public XUnitLogger(string name, ITestOutputHelperAccessor accessor, XUnitLoggerOptions options) { Name = name ?? throw new ArgumentNullException(nameof(name)); - _outputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); + _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); Filter = options?.Filter ?? ((category, logLevel) => true); IncludeScopes = options?.IncludeScopes ?? false; @@ -143,6 +157,13 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// The exception related to this message. public virtual void WriteMessage(LogLevel logLevel, int eventId, string message, Exception exception) { + ITestOutputHelper outputHelper = _accessor.OutputHelper; + + if (outputHelper == null) + { + return; + } + StringBuilder logBuilder = _logBuilder; _logBuilder = null; @@ -186,7 +207,7 @@ public virtual void WriteMessage(LogLevel logLevel, int eventId, string message, } string formatted = logBuilder.ToString(); - _outputHelper.WriteLine($"[{Clock():u}] {logLevelString}{formatted}"); + outputHelper.WriteLine($"[{Clock():u}] {logLevelString}{formatted}"); logBuilder.Clear(); diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.cs b/src/Logging.XUnit/XUnitLoggerExtensions.cs index 56bca335..f87745a8 100644 --- a/src/Logging.XUnit/XUnitLoggerExtensions.cs +++ b/src/Logging.XUnit/XUnitLoggerExtensions.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using MartinCostello.Logging.XUnit; +using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit.Abstractions; namespace Microsoft.Extensions.Logging @@ -14,6 +15,91 @@ namespace Microsoft.Extensions.Logging [EditorBrowsable(EditorBrowsableState.Never)] public static class XUnitLoggerExtensions { + /// + /// Adds an xunit logger to the logging builder. + /// + /// The to use. + /// + /// The instance of specified by . + /// + /// + /// is . + /// + public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.AddXUnit(new AmbientTestOutputHelperAccessor(), (_) => { }); + } + + /// + /// Adds an xunit logger to the logging builder. + /// + /// The to use. + /// The to use. + /// + /// The instance of specified by . + /// + /// + /// or is . + /// + public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, ITestOutputHelperAccessor accessor) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (accessor == null) + { + throw new ArgumentNullException(nameof(accessor)); + } + + return builder.AddXUnit(accessor, (_) => { }); + } + + /// + /// Adds an xunit logger to the logging builder. + /// + /// The to use. + /// The to use. + /// A delegate to a method to use to configure the logging options. + /// + /// The instance of specified by . + /// + /// + /// , OR is . + /// + public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, ITestOutputHelperAccessor accessor, Action configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (accessor == null) + { + throw new ArgumentNullException(nameof(accessor)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var options = new XUnitLoggerOptions(); + + configure(options); + + builder.AddProvider(new XUnitLoggerProvider(accessor, options)); + builder.Services.TryAddSingleton(accessor); + + return builder; + } + /// /// Adds an xunit logger to the logging builder. /// diff --git a/src/Logging.XUnit/XUnitLoggerProvider.cs b/src/Logging.XUnit/XUnitLoggerProvider.cs index d5307ac1..c6ccaab5 100644 --- a/src/Logging.XUnit/XUnitLoggerProvider.cs +++ b/src/Logging.XUnit/XUnitLoggerProvider.cs @@ -13,9 +13,9 @@ namespace MartinCostello.Logging.XUnit public class XUnitLoggerProvider : ILoggerProvider { /// - /// The to use. This field is readonly. + /// The to use. This field is readonly. /// - private readonly ITestOutputHelper _outputHelper; + private readonly ITestOutputHelperAccessor _accessor; /// /// The to use. This field is readonly. @@ -31,8 +31,21 @@ public class XUnitLoggerProvider : ILoggerProvider /// or is . /// public XUnitLoggerProvider(ITestOutputHelper outputHelper, XUnitLoggerOptions options) + : this(new TestOutputHelperAccessor(outputHelper), options) { - _outputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use. + /// The options to use for logging to xunit. + /// + /// or is . + /// + public XUnitLoggerProvider(ITestOutputHelperAccessor accessor, XUnitLoggerOptions options) + { + _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); _options = options ?? throw new ArgumentNullException(nameof(options)); } @@ -45,7 +58,7 @@ public XUnitLoggerProvider(ITestOutputHelper outputHelper, XUnitLoggerOptions op } /// - public virtual ILogger CreateLogger(string categoryName) => new XUnitLogger(categoryName, _outputHelper, _options); + public virtual ILogger CreateLogger(string categoryName) => new XUnitLogger(categoryName, _accessor, _options); /// public void Dispose() diff --git a/tests/Logging.XUnit.Tests/Integration/HttpApplicationTests.cs b/tests/Logging.XUnit.Tests/Integration/HttpApplicationTests.cs new file mode 100644 index 00000000..4a183ca7 --- /dev/null +++ b/tests/Logging.XUnit.Tests/Integration/HttpApplicationTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit.Integration +{ + [Collection(HttpServerCollection.Name)] + public sealed class HttpApplicationTests : IDisposable + { + public HttpApplicationTests(HttpServerFixture fixture, ITestOutputHelper outputHelper) + { + Fixture = fixture; + Fixture.SetOutputHelper(outputHelper); + } + + private HttpServerFixture Fixture { get; } + + public void Dispose() + { + Fixture.ClearOutputHelper(); + } + + [Fact] + public async Task Http_Get_Many() + { + // Arrange + using (var httpClient = Fixture.CreateClient()) + { + // Act + using (var response = await httpClient.GetAsync("api/values")) + { + // Assert + response.IsSuccessStatusCode.ShouldBeTrue(); + } + } + } + + [Fact] + public async Task Http_Get_Single() + { + // Arrange + using (var httpClient = Fixture.CreateClient()) + { + // Act + using (var response = await httpClient.GetAsync("api/values/a")) + { + // Assert + response.IsSuccessStatusCode.ShouldBeTrue(); + } + } + } + + [Fact] + public async Task Http_Post() + { + // Arrange + using (var httpClient = Fixture.CreateClient()) + { + // Act + using (var response = await httpClient.PostAsJsonAsync("api/values", "d")) + { + // Assert + response.IsSuccessStatusCode.ShouldBeTrue(); + } + } + } + + [Fact] + public async Task Http_Put() + { + // Arrange + using (var httpClient = Fixture.CreateClient()) + { + // Act + using (var response = await httpClient.PutAsJsonAsync("api/values/d", "d")) + { + // Assert + response.IsSuccessStatusCode.ShouldBeTrue(); + } + } + } + + [Fact] + public async Task Http_Delete() + { + // Arrange + using (var httpClient = Fixture.CreateClient()) + { + // Act + using (var response = await httpClient.DeleteAsync("api/values/d")) + { + // Assert + response.IsSuccessStatusCode.ShouldBeTrue(); + } + } + } + } +} diff --git a/tests/Logging.XUnit.Tests/Integration/HttpServerCollection.cs b/tests/Logging.XUnit.Tests/Integration/HttpServerCollection.cs new file mode 100644 index 00000000..7bee32ca --- /dev/null +++ b/tests/Logging.XUnit.Tests/Integration/HttpServerCollection.cs @@ -0,0 +1,19 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Xunit; + +namespace MartinCostello.Logging.XUnit.Integration +{ + /// + /// A class representing the collection fixture for an HTTP server. This class cannot be inherited. + /// + [CollectionDefinition(Name)] + public sealed class HttpServerCollection : ICollectionFixture + { + /// + /// The name of the test fixture. + /// + public const string Name = "HTTP server collection"; + } +} diff --git a/tests/Logging.XUnit.Tests/Integration/HttpServerFixture.cs b/tests/Logging.XUnit.Tests/Integration/HttpServerFixture.cs new file mode 100644 index 00000000..132ca266 --- /dev/null +++ b/tests/Logging.XUnit.Tests/Integration/HttpServerFixture.cs @@ -0,0 +1,53 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SampleApp; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit.Integration +{ + /// + /// A test fixture representing an HTTP server hosting the sample application. This class cannot be inherited. + /// + public sealed class HttpServerFixture : WebApplicationFactory + { + /// + /// Initializes a new instance of the class. + /// + public HttpServerFixture() + : base() + { + // HACK Force HTTP server startup + using (CreateDefaultClient()) + { + } + } + + /// + /// Clears the current . + /// + public void ClearOutputHelper() + { + Server.Host.Services.GetRequiredService().OutputHelper = null; + } + + /// + /// Sets the to use. + /// + /// The to use. + public void SetOutputHelper(ITestOutputHelper value) + { + Server.Host.Services.GetRequiredService().OutputHelper = value; + } + + /// + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureLogging((p) => p.AddXUnit()); + } + } +} diff --git a/tests/Logging.XUnit.Tests/IntegrationTests.cs b/tests/Logging.XUnit.Tests/IntegrationTests.cs index b8e109a6..2aed796c 100644 --- a/tests/Logging.XUnit.Tests/IntegrationTests.cs +++ b/tests/Logging.XUnit.Tests/IntegrationTests.cs @@ -13,7 +13,7 @@ namespace MartinCostello.Logging.XUnit public static class IntegrationTests { [Fact] - public static void Can_Configure_xunit_For_ILoggerBuilder() + public static void Can_Configure_xunit_For_ILoggerBuilder_TestOutputHelper() { // Arrange var mock = new Mock(); @@ -28,7 +28,7 @@ public static void Can_Configure_xunit_For_ILoggerBuilder() } [Fact] - public static void Can_Configure_xunit_For_ILoggerBuilder_With_Configuration() + public static void Can_Configure_xunit_For_ILoggerBuilder_TestOutputHelper_With_Configuration() { // Arrange var mock = new Mock(); @@ -49,6 +49,62 @@ public static void Can_Configure_xunit_For_ILoggerBuilder_With_Configuration() mock.Verify((p) => p.WriteLine(It.IsNotNull()), Times.Once()); } + [Fact] + public static void Can_Configure_xunit_For_ILoggerBuilderAccessor_TestOutputHelper() + { + // Arrange + var mockOutputHelper = new Mock(); + var outputHelper = mockOutputHelper.Object; + + var mockAccessor = new Mock(); + + mockAccessor + .Setup((p) => p.OutputHelper) + .Returns(outputHelper); + + var accessor = mockAccessor.Object; + + var logger = BootstrapBuilder((builder) => builder.AddXUnit(accessor)); + + // Act + logger.LogError("This is a brand new problem, a problem without any clues."); + logger.LogInformation("If you know the clues, it's easy to get through."); + + // Assert + mockOutputHelper.Verify((p) => p.WriteLine(It.IsNotNull()), Times.Exactly(2)); + } + + [Fact] + public static void Can_Configure_xunit_For_ILoggerBuilder_TestOutputHelperAccessor_With_Configuration() + { + // Arrange + var mockOutputHelper = new Mock(); + var outputHelper = mockOutputHelper.Object; + + var mockAccessor = new Mock(); + + mockAccessor + .Setup((p) => p.OutputHelper) + .Returns(outputHelper); + + var accessor = mockAccessor.Object; + + var logger = BootstrapBuilder( + (builder) => + { + builder.AddXUnit( + mockOutputHelper.Object, + (options) => options.Filter = (_, level) => level >= LogLevel.Error); + }); + + // Act + logger.LogError("This is a brand new problem, a problem without any clues."); + logger.LogTrace("If you know the clues, it's easy to get through."); + + // Assert + mockOutputHelper.Verify((p) => p.WriteLine(It.IsNotNull()), Times.Once()); + } + [Fact] public static void Can_Configure_xunit_For_ILoggerFactory() { @@ -157,6 +213,28 @@ public static void Can_Configure_xunit_For_ILoggerFactory_With_Configure_Options mock.Verify((p) => p.WriteLine(It.IsNotNull()), Times.Once()); } + [Fact] + public static void Can_Configure_xunit_For_ILoggerBuilder() + { + // Arrange + var serviceProvider = new ServiceCollection() + .AddLogging((builder) => builder.AddXUnit()) + .BuildServiceProvider(); + + var mock = new Mock(); + + serviceProvider.GetRequiredService().OutputHelper = mock.Object; + + var logger = serviceProvider.GetRequiredService>(); + + // Act + logger.LogError("This is a brand new problem, a problem without any clues."); + logger.LogInformation("If you know the clues, it's easy to get through."); + + // Assert + mock.Verify((p) => p.WriteLine(It.IsNotNull()), Times.Exactly(2)); + } + private static ILogger BootstrapBuilder(Action configure) { return new ServiceCollection() diff --git a/tests/Logging.XUnit.Tests/MartinCostello.Logging.XUnit.Tests.csproj b/tests/Logging.XUnit.Tests/MartinCostello.Logging.XUnit.Tests.csproj index a2ae0ee4..d8397156 100644 --- a/tests/Logging.XUnit.Tests/MartinCostello.Logging.XUnit.Tests.csproj +++ b/tests/Logging.XUnit.Tests/MartinCostello.Logging.XUnit.Tests.csproj @@ -1,8 +1,8 @@ - + Tests for MartinCostello.Logging.XUnit. false - $(NoWarn);CA1707 + $(NoWarn);CA1707;CA2007;CA2234 true MartinCostello.Logging.XUnit $(Description) @@ -13,8 +13,10 @@ + + diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs index 60de11eb..fad3e2ea 100644 --- a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs +++ b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs @@ -18,13 +18,20 @@ public static void AddXUnit_For_ILoggerBuilder_Validates_Parameters() // Arrange var builder = Mock.Of(); var outputHelper = Mock.Of(); + var accessor = Mock.Of(); // Act and Assert + Assert.Throws("builder", () => (null as ILoggingBuilder).AddXUnit()); Assert.Throws("builder", () => (null as ILoggingBuilder).AddXUnit(outputHelper)); Assert.Throws("builder", () => (null as ILoggingBuilder).AddXUnit(outputHelper, ConfigureAction)); + Assert.Throws("builder", () => (null as ILoggingBuilder).AddXUnit(accessor)); + Assert.Throws("builder", () => (null as ILoggingBuilder).AddXUnit(accessor, ConfigureAction)); + Assert.Throws("accessor", () => builder.AddXUnit(null as ITestOutputHelperAccessor)); + Assert.Throws("accessor", () => builder.AddXUnit(null as ITestOutputHelperAccessor, ConfigureAction)); Assert.Throws("outputHelper", () => builder.AddXUnit(null as ITestOutputHelper)); - Assert.Throws("outputHelper", () => builder.AddXUnit(null, ConfigureAction)); + Assert.Throws("outputHelper", () => builder.AddXUnit(null as ITestOutputHelper, ConfigureAction)); Assert.Throws("configure", () => builder.AddXUnit(outputHelper, null as Action)); + Assert.Throws("configure", () => builder.AddXUnit(accessor, null as Action)); } [Fact] diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs index c6c6fcd9..37e4eb35 100644 --- a/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs +++ b/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs @@ -17,11 +17,14 @@ public static void XUnitLoggerProvider_Constructor_Validates_Parameters() { // Arrange var outputHelper = Mock.Of(); + var accessor = Mock.Of(); var options = new XUnitLoggerOptions(); // Act and Assert - Assert.Throws("outputHelper", () => new XUnitLoggerProvider(null, options)); + Assert.Throws("outputHelper", () => new XUnitLoggerProvider(null as ITestOutputHelper, options)); + Assert.Throws("accessor", () => new XUnitLoggerProvider(null as ITestOutputHelperAccessor, options)); Assert.Throws("options", () => new XUnitLoggerProvider(outputHelper, null)); + Assert.Throws("options", () => new XUnitLoggerProvider(accessor, null)); } [Fact] diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs index 1d6ba5ac..6b937c5a 100644 --- a/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs +++ b/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs @@ -26,7 +26,8 @@ public static void XUnitLogger_Validates_Parameters() // Act and Assert Assert.Throws("name", () => new XUnitLogger(null, outputHelper, options)); - Assert.Throws("outputHelper", () => new XUnitLogger(name, null, options)); + Assert.Throws("outputHelper", () => new XUnitLogger(name, null as ITestOutputHelper, options)); + Assert.Throws("accessor", () => new XUnitLogger(name, null as ITestOutputHelperAccessor, options)); // Arrange var logger = new XUnitLogger(name, outputHelper, options); @@ -228,6 +229,24 @@ public static void XUnitLogger_Log_Does_Nothing_If_Empty_Message_And_No_Exceptio mock.Verify((p) => p.WriteLine(It.IsAny()), Times.Never()); } + [Fact] + public static void XUnitLogger_Log_Does_Nothing_If_No_OutputHelper() + { + // Arrange + string name = "MyName"; + var accessor = Mock.Of(); + + var options = new XUnitLoggerOptions() + { + Filter = FilterTrue, + }; + + var logger = new XUnitLogger(name, accessor, options); + + // Act (no Assert) + logger.Log(LogLevel.Information, new EventId(2), "state", null, Formatter); + } + [Fact] public static void XUnitLogger_Log_Logs_Message_If_Only_Exception() { diff --git a/tests/SampleApp/Controllers/ValuesController.cs b/tests/SampleApp/Controllers/ValuesController.cs new file mode 100644 index 00000000..05419dce --- /dev/null +++ b/tests/SampleApp/Controllers/ValuesController.cs @@ -0,0 +1,40 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace SampleApp.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class ValuesController : ControllerBase + { + [HttpGet] + public ActionResult> Get() + { + return new string[] { "a", "b", "c" }; + } + + [HttpGet("{id}")] + public ActionResult Get(string id) + { + return "value"; + } + + [HttpPost] + public void Post([FromBody] string value) + { + } + + [HttpPut("{id}")] + public void Put(string id, [FromBody] string value) + { + } + + [HttpDelete("{id}")] + public void Delete(string id) + { + } + } +} diff --git a/tests/SampleApp/Program.cs b/tests/SampleApp/Program.cs new file mode 100644 index 00000000..b06d8246 --- /dev/null +++ b/tests/SampleApp/Program.cs @@ -0,0 +1,20 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace SampleApp +{ + public static class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/tests/SampleApp/Properties/launchSettings.json b/tests/SampleApp/Properties/launchSettings.json new file mode 100644 index 00000000..4aad6877 --- /dev/null +++ b/tests/SampleApp/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52657", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "SampleApp": { + "commandName": "SampleApp", + "launchBrowser": false, + "launchUrl": "api/values", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/tests/SampleApp/SampleApp.csproj b/tests/SampleApp/SampleApp.csproj new file mode 100644 index 00000000..0125f52e --- /dev/null +++ b/tests/SampleApp/SampleApp.csproj @@ -0,0 +1,10 @@ + + + false + $(NoWarn);CA1801;CA1822 + netcoreapp2.1 + + + + + diff --git a/tests/SampleApp/Startup.cs b/tests/SampleApp/Startup.cs new file mode 100644 index 00000000..04773acc --- /dev/null +++ b/tests/SampleApp/Startup.cs @@ -0,0 +1,36 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace SampleApp +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMvc(); + } + } +} diff --git a/tests/SampleApp/appsettings.Development.json b/tests/SampleApp/appsettings.Development.json new file mode 100644 index 00000000..e203e940 --- /dev/null +++ b/tests/SampleApp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/tests/SampleApp/appsettings.json b/tests/SampleApp/appsettings.json new file mode 100644 index 00000000..def9159a --- /dev/null +++ b/tests/SampleApp/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +}