diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index c06558f9..28ef6182 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - 'GitVersion.yml' + - '!mkdocs.yml' - 'src/**' - '!src/qodana.yml' - '.github/actions/**' @@ -13,12 +14,14 @@ on: - '!.github/workflows/qodana.yml' - '!.github/workflows/semgrep.yml' - '!.github/workflows/snyk.yml' + - '!.github/workflows/docs.yml' types: [opened, synchronize, reopened] push: branches: - 'master' paths: - 'GitVersion.yml' + - '!mkdocs.yml' - 'src/**' - '!src/qodana.yml' - '.github/actions/**' @@ -28,6 +31,7 @@ on: - '!.github/workflows/qodana.yml' - '!.github/workflows/semgrep.yml' - '!.github/workflows/snyk.yml' + - '!.github/workflows/docs.yml' workflow_dispatch: inputs: buildAutoFakeItEasy: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..e7e2942f --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,77 @@ +name: Build Documentation + +on: + push: + branches: [ master ] + paths: + - 'docs/**' + - 'mkdocs.yml' + - '.github/workflows/docs.yml' + pull_request: + branches: [ master ] + paths: + - 'docs/**' + - 'mkdocs.yml' + - '.github/workflows/docs.yml' + workflow_dispatch: + +defaults: + run: + shell: pwsh + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + pages: write + id-token: write + steps: + - name: 📥 checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 🐍 setup python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: 📦 install mkdocs and material theme + run: | + $ErrorActionPreference = 'stop' + pip install mkdocs-material + if ($LastExitCode -ne 0) { + throw "pip install failed with exit code $LastExitCode" + } + + - name: 🏗️ build documentation + run: | + $ErrorActionPreference = 'stop' + mkdocs build + if ($LastExitCode -ne 0) { + throw "mkdocs build failed with exit code $LastExitCode" + } + + - name: 📤 upload pages artifact + id: artifacts + uses: actions/upload-pages-artifact@v3 + with: + path: ./site + + deploy: + environment: + name: github-pages + url: ${{ steps.artifacts.outputs.page_url }} + runs-on: ubuntu-latest + needs: [build] + if: ${{ github.ref_name == 'master' && needs.build.result == 'success' }} + steps: + - name: 📤 Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/README.md b/README.md index 08d43c8e..a69e1617 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI/CD](https://img.shields.io/github/actions/workflow/status/Accenture/AutoFixture.XUnit2.AutoMock/cicd.yml?logo=githubactions&logoColor=white&label=ci%2Fcd)](https://github.com/Accenture/AutoFixture.XUnit2.AutoMock/actions/workflows/cicd.yml) [![codecov](https://codecov.io/gh/Accenture/AutoFixture.XUnit2.AutoMock/branch/master/graph/badge.svg)](https://codecov.io/gh/Accenture/AutoFixture.XUnit2.AutoMock) [![Mutation testing](https://img.shields.io/endpoint?style=flat&label=stryker&logo=stryker&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FAccenture%2FAutoFixture.XUnit2.AutoMock%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/Accenture/AutoFixture.XUnit2.AutoMock/master) [![qodana](https://img.shields.io/github/actions/workflow/status/Accenture/AutoFixture.XUnit2.AutoMock/qodana.yml?style=flat&logo=resharper&label=qodana)](https://github.com/Accenture/AutoFixture.XUnit2.AutoMock/actions/workflows/qodana.yml) [![License: MIT](https://img.shields.io/github/license/Accenture/AutoFixture.XUnit2.AutoMock?label=license&color=brightgreen)](https://opensource.org/licenses/MIT) [![fossa](https://app.fossa.com/api/projects/git%2Bgithub.com%2FObjectivityLtd%2FAutoFixture.XUnit2.AutoMock.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FObjectivityLtd%2FAutoFixture.XUnit2.AutoMock?ref=badge_shield) -Accelerates preparation of mocked structures for unit tests under [XUnit2](http://xunit.github.io/) by configuring [AutoFixture](https://github.com/AutoFixture/AutoFixture) data generation to use a mocking library of your choice. Gracefully handles recursive structures by omitting recursions. +Accelerates preparation of mocked structures for unit tests under [XUnit2](http://xunit.net/) by configuring [AutoFixture](https://github.com/AutoFixture/AutoFixture) data generation to use a mocking library of your choice. Gracefully handles recursive structures by omitting recursions. It provides the following mocking attributes: diff --git a/docs/attributes/auto-mock-data-attribute.md b/docs/attributes/auto-mock-data-attribute.md new file mode 100644 index 00000000..ba7f82da --- /dev/null +++ b/docs/attributes/auto-mock-data-attribute.md @@ -0,0 +1,30 @@ +# AutoMockData Attribute + +Provides auto-generated data specimens generated by [AutoFixture](https://github.com/AutoFixture/AutoFixture) with a mocking library as an extension to xUnit.net's `Theory` attribute. + +## Arguments + +- `IgnoreVirtualMembers` - disables generation of members marked as `virtual` (default: `false`) + +## Example + +```csharp +[Theory] +[AutoMockData] +public void GivenCurrencyConverter_WhenConvertToPln_ThenMustReturnCorrectConvertedAmount( + string testCurrencySymbol, + [Frozen] ICurrencyExchangeProvider currencyProvider, + CurrencyConverter currencyConverter) +{ + // Arrange + Mock.Get(currencyProvider) + .Setup(cp => cp.GetCurrencyExchangeRate(testCurrencySymbol)) + .Returns(100M); + + // Act + decimal result = currencyConverter.ConvertToPln(testCurrencySymbol, 100M); + + // Assert + Assert.Equal(10000M, result); +} +``` diff --git a/docs/attributes/customize-with-attribute.md b/docs/attributes/customize-with-attribute.md new file mode 100644 index 00000000..59af45ad --- /dev/null +++ b/docs/attributes/customize-with-attribute.md @@ -0,0 +1,44 @@ +# CustomizeWith Attribute + +An attribute that can be applied to parameters in an `AutoDataAttribute`-driven `Theory` to apply additional customization when the `IFixture` creates an instance of that type. + +## Arguments + +- `IncludeParameterType` - indicates whether attribute target parameter `Type` should be included as a first argument when creating customization; by default set to `false` + +**Caution:** Order is important! Applying `CustomizeWith` attribute to the subsequent parameter makes preceding parameters of the same type to be created without specified customization and the particular parameter with the specified customization. + +## Example + +```csharp +public class LocalDatesCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Register(() => LocalDate.FromDateTime(fixture.Create())); + } +} +``` + +```csharp +[Theory] +[InlineAutoMockData("USD")] +[InlineAutoMockData("EUR")] +public void GivenCurrencyConverter_WhenConvertToPlnAtParticularDay_ThenMustReturnCorrectConvertedAmount( + string testCurrencySymbol, + [CustomizeWith(typeof(LocalDatesCustomization))] LocalDate day, + [Frozen] ICurrencyExchangeProvider currencyProvider, + CurrencyConverter currencyConverter) +{ + // Arrange + Mock.Get(currencyProvider) + .Setup(cp => cp.GetCurrencyExchangeRate(testCurrencySymbol, day)) + .Returns(100M); + + // Act + decimal result = currencyConverter.ConvertToPln(testCurrencySymbol, 100M, day); + + // Assert + Assert.Equal(10000M, result); +} +``` diff --git a/docs/attributes/customize-with-t-attribute.md b/docs/attributes/customize-with-t-attribute.md new file mode 100644 index 00000000..62e4f214 --- /dev/null +++ b/docs/attributes/customize-with-t-attribute.md @@ -0,0 +1,53 @@ +# CustomizeWith\ Attribute + +A generic version of the `CustomizeWith` attribute has been introduced for ease of use. The same rules apply as for the non-generic version. + +## Example + +```csharp +public class EmptyCollectionCustomization : ICustomization +{ + public EmptyCollectionCustomization(Type reflectedType) + { + this.ReflectedType = reflectedType; + } + + public Type ReflectedType { get; } + + public void Customize(IFixture fixture) + { + var emptyArray = Array.CreateInstance(this.ReflectedType.GenericTypeArguments.Single(), 0); + + fixture.Customizations.Add( + new FilteringSpecimenBuilder( + new FixedBuilder(emptyArray), + new ExactTypeSpecification(this.ReflectedType))); + } +} +``` + +```csharp +public sealed class EmptyCollectionAttribute : CustomizeWithAttribute +{ + public EmptyCollectionAttribute() + { + this.IncludeParameterType = true; + } +} +``` + +```csharp +[Theory] +[AutoData] +public void CustomizeWithAttributeUsage( + IList firstStore, + [EmptyCollection] IList secondStore, + IList thirdStore, + IList fourthStore) +{ + Assert.NotEmpty(firstStore); + Assert.Empty(secondStore); + Assert.Empty(thirdStore); + Assert.NotEmpty(fourthStore); +} +``` diff --git a/docs/attributes/except-attribute.md b/docs/attributes/except-attribute.md new file mode 100644 index 00000000..182c2fa4 --- /dev/null +++ b/docs/attributes/except-attribute.md @@ -0,0 +1,13 @@ +# Except Attribute + +Ensures that values from outside the specified list will be generated. + +```csharp +[Theory] +[AutoData] +public void ExceptAttributeUsage( + [Except(DayOfWeek.Saturday, DayOfWeek.Sunday)] DayOfWeek workday) +{ + Assert.True(workday is >= DayOfWeek.Monday and <= DayOfWeek.Friday); +} +``` diff --git a/docs/attributes/ignore-virtual-members-attribute.md b/docs/attributes/ignore-virtual-members-attribute.md new file mode 100644 index 00000000..207c5f89 --- /dev/null +++ b/docs/attributes/ignore-virtual-members-attribute.md @@ -0,0 +1,36 @@ +# IgnoreVirtualMembers Attribute + +An attribute that can be applied to parameters in an `AutoDataAttribute`-driven `Theory` to indicate that the parameter value should not have `virtual` properties populated when the `IFixture` creates an instance of that type. + +This attribute allows disabling the generation of members marked as `virtual` on a decorated type, whereas `IgnoreVirtualMembers` arguments of mocking attributes mentioned above disable such a generation for all types created by `IFixture`. + +**Caution:** Order is important! Applying `IgnoreVirtualMembers` attribute to the subsequent parameter makes preceding parameters of the same type to have `virtual` properties populated and the particular parameter with the following ones of the same type to have `virtual` properties unpopulated. + +## Example + +```csharp +public class User +{ + public string Name { get; set; } + public virtual Address Address { get; set; } +} +``` + +```csharp +[Theory] +[AutoData] +public void IgnoreVirtualMembersUsage( + User firstUser, + [IgnoreVirtualMembers] User secondUser, + User thirdUser) +{ + Assert.NotNull(firstUser.Name); + Assert.NotNull(firstUser.Address); + + Assert.NotNull(secondUser.Name); + Assert.Null(secondUser.Address); + + Assert.NotNull(thirdUser.Name); + Assert.Null(thirdUser.Address); +} +``` diff --git a/docs/attributes/index.md b/docs/attributes/index.md new file mode 100644 index 00000000..92bc30d4 --- /dev/null +++ b/docs/attributes/index.md @@ -0,0 +1,22 @@ +# Attributes Overview + +This section describes the main attributes provided by AutoFixture.XUnit2.AutoMock for use in your xUnit tests: + +## Test Method Attributes + +- [AutoMockData](auto-mock-data-attribute.md): Provides auto-generated data specimens using AutoFixture and a mocking library. +- [InlineAutoMockData](inline-auto-mock-data-attribute.md): Combines inline values with auto-generated data specimens. +- [MemberAutoMockData](member-auto-mock-data-attribute.md): Uses static members as data sources, combined with auto-generated data. + +## Parameter Configuration Attributes + +- [IgnoreVirtualMembers](ignore-virtual-members-attribute.md): Disables generation of virtual members for a parameter or globally. +- [CustomizeWith](customize-with-attribute.md): Applies additional customization to a parameter. +- [CustomizeWith\](customize-with-t-attribute.md): Generic version of CustomizeWith for ease of use. + +## Data Filtering Attributes + +- [Except](except-attribute.md): Ensures values from outside the specified list will be generated. +- [PickFromRange](pick-from-range-attribute.md): Ensures only values from a specified range will be generated. +- [PickNegative](pick-negative-attribute.md): Ensures only negative values will be generated. +- [PickFromValues](pick-from-values-attribute.md): Ensures only values from the specified list will be generated. diff --git a/docs/attributes/inline-auto-mock-data-attribute.md b/docs/attributes/inline-auto-mock-data-attribute.md new file mode 100644 index 00000000..2d4c2e05 --- /dev/null +++ b/docs/attributes/inline-auto-mock-data-attribute.md @@ -0,0 +1,34 @@ +# InlineAutoMockData Attribute + +Provides a data source for a `Theory`, with the data coming from inline values combined with auto-generated data specimens generated by [AutoFixture](https://github.com/AutoFixture/AutoFixture) with a mocking library. + +## Arguments + +- `IgnoreVirtualMembers` - disables generation of members marked as `virtual` (default: `false`) + +## Example + +```csharp +[Theory] +[InlineAutoMockData("USD", 3, 10, 30)] +[InlineAutoMockData("EUR", 4, 20, 80)] +public void GivenCurrencyConverter_WhenConvertToPln_ThenMustReturnCorrectConvertedAmount( + string testCurrencySymbol, + decimal exchangeRate, + decimal currencyAmount, + decimal expectedPlnAmount, + [Frozen] ICurrencyExchangeProvider currencyProvider, + CurrencyConverter currencyConverter) +{ + // Arrange + Mock.Get(currencyProvider) + .Setup(cp => cp.GetCurrencyExchangeRate(testCurrencySymbol)) + .Returns(exchangeRate); + + // Act + decimal result = currencyConverter.ConvertToPln(testCurrencySymbol, currencyAmount); + + // Assert + Assert.Equal(expectedPlnAmount, result); +} +``` diff --git a/docs/attributes/member-auto-mock-data-attribute.md b/docs/attributes/member-auto-mock-data-attribute.md new file mode 100644 index 00000000..5c6994c6 --- /dev/null +++ b/docs/attributes/member-auto-mock-data-attribute.md @@ -0,0 +1,58 @@ +# MemberAutoMockData Attribute + +Provides a data source for a `Theory`, with the data coming from one of the following sources: + +- A static property +- A static field +- A static method (with parameters) + +combined with auto-generated data specimens generated by [AutoFixture](https://github.com/AutoFixture/AutoFixture) with a mocking library. + +The member must return type compatible with `Enumerable` containing the test data. + +**Caution:** The property is completely enumerated by `.ToList()` before any test is run. Hence it should return independent object sets. + +## Arguments + +- `IgnoreVirtualMembers` - disables generation of members marked as `virtual` (default: `false`) +- `ShareFixture` - indicates whether to share a `fixture` across all data items should be used or new one; by default set to `true` + +## Example + +```csharp +public class CurrencyConverterFixture +{ + public static IEnumerable CurrencyConversionRatesWithResult() + { + return new List + { + new object[] { "USD", 3M, 10M, 30M }, + new object[] { "EUR", 4M, 20M, 80M } + }; + } +} +``` + +```csharp +[Theory] +[MemberAutoMockData("CurrencyConversionRatesWithResult", MemberType = typeof(CurrencyConverterFixture))] +public void GivenCurrencyConverter_WhenConvertToPln_ThenMustReturnCorrectConvertedAmount( + string testCurrencySymbol, + decimal exchangeRate, + decimal currencyAmount, + decimal expectedPlnAmount, + [Frozen] ICurrencyExchangeProvider currencyProvider, + CurrencyConverter currencyConverter) +{ + // Arrange + Mock.Get(currencyProvider) + .Setup(cp => cp.GetCurrencyExchangeRate(testCurrencySymbol)) + .Returns(exchangeRate); + + // Act + decimal result = currencyConverter.ConvertToPln(testCurrencySymbol, currencyAmount); + + // Assert + Assert.Equal(expectedPlnAmount, result); +} +``` diff --git a/docs/attributes/pick-from-range-attribute.md b/docs/attributes/pick-from-range-attribute.md new file mode 100644 index 00000000..6238212f --- /dev/null +++ b/docs/attributes/pick-from-range-attribute.md @@ -0,0 +1,13 @@ +# PickFromRange Attribute + +Ensures that only values from a specified range will be generated. + +```csharp +[Theory] +[AutoData] +public void RangeAttributeUsage( + [PickFromRange(11, 19)] int teenagerAge) +{ + Assert.True(teenagerAge is > 11 and < 19); +} +``` diff --git a/docs/attributes/pick-from-values-attribute.md b/docs/attributes/pick-from-values-attribute.md new file mode 100644 index 00000000..f3004fef --- /dev/null +++ b/docs/attributes/pick-from-values-attribute.md @@ -0,0 +1,14 @@ +# PickFromValues Attribute + +Ensures that only values from the specified list will be generated. + +```csharp +[Theory] +[AutoData] +public void ValuesAttributeUsage( + [PickFromValues(DayOfWeek.Saturday, DayOfWeek.Sunday)] HashSet weekend) +{ + var weekendDays = new[] { DayOfWeek.Saturday, DayOfWeek.Sunday }; + Assert.Equivalent(weekendDays, weekend); +} +``` diff --git a/docs/attributes/pick-negative-attribute.md b/docs/attributes/pick-negative-attribute.md new file mode 100644 index 00000000..8b3c9f37 --- /dev/null +++ b/docs/attributes/pick-negative-attribute.md @@ -0,0 +1,15 @@ +# PickNegative Attribute + +Ensures that only negative values will be generated. + +**Caution:** It will throw an exception when used on an unsupported type or on one which does not accept negative values. + +```csharp +[Theory] +[AutoData] +public void NegativeAttributeUsage( + [PickNegative] int negativeNumber) +{ + Assert.True(negativeNumber < 0); +} +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..5ca1af67 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,9 @@ +# About AutoFixture.XUnit2.AutoMock + +AutoFixture.XUnit2.AutoMock is a free, open source tool that accelerates the preparation of mocked structures for unit tests under [XUnit2](http://xunit.net/) by configuring [AutoFixture](https://github.com/AutoFixture/AutoFixture) to work with the mocking library of your choice. It gracefully handles recursive structures by omitting recursions. + +## Table of Contents + +- [Supported Mocking Libraries](supported-mocking-libraries.md) +- [Attributes](attributes/index.md) +- [Tips and Tricks](tips-and-tricks.md) diff --git a/docs/supported-mocking-libraries.md b/docs/supported-mocking-libraries.md new file mode 100644 index 00000000..7b2594d8 --- /dev/null +++ b/docs/supported-mocking-libraries.md @@ -0,0 +1,7 @@ +# Supported Mocking Libraries + +| Mocking library | Corresponding NuGet package | +| ---------------------------------------------------------:|:--------------------------- | +| [Moq](https://github.com/moq/moq4) | [![AutoMoq](https://img.shields.io/nuget/v/Objectivity.AutoFixture.XUnit2.AutoMoq.svg?logo=nuget&style=flat-square&label=AutoMoq)![Downloads](https://img.shields.io/nuget/dt/Objectivity.AutoFixture.XUnit2.AutoMoq.svg?style=flat-square&label)](https://www.nuget.org/packages/Objectivity.AutoFixture.XUnit2.AutoMoq/) | +| [NSubstitute](https://github.com/nsubstitute/NSubstitute) | [![AutoNSubstitute](https://img.shields.io/nuget/v/Objectivity.AutoFixture.XUnit2.AutoNSubstitute.svg?logo=nuget&style=flat-square&label=AutoNSubstitute)![Downloads](https://img.shields.io/nuget/dt/Objectivity.AutoFixture.XUnit2.AutoNSubstitute.svg?style=flat-square&label)](https://www.nuget.org/packages/Objectivity.AutoFixture.XUnit2.AutoNSubstitute/) | +| [FakeItEasy](https://github.com/FakeItEasy/FakeItEasy) | [![AutoFakeItEasy](https://img.shields.io/nuget/v/Objectivity.AutoFixture.XUnit2.AutoFakeItEasy.svg?logo=nuget&style=flat-square&label=AutoFakeItEasy)![Downloads](https://img.shields.io/nuget/dt/Objectivity.AutoFixture.XUnit2.AutoFakeItEasy.svg?style=flat-square&label)](https://www.nuget.org/packages/Objectivity.AutoFixture.XUnit2.AutoFakeItEasy/) | diff --git a/docs/tips-and-tricks.md b/docs/tips-and-tricks.md new file mode 100644 index 00000000..b90227b9 --- /dev/null +++ b/docs/tips-and-tricks.md @@ -0,0 +1,44 @@ +# Tips and Tricks + +## Fixture Injection + +You can inject the same instance of `IFixture` into a test method by adding the mentioned interface as an argument of the test method. + +```csharp +[Theory] +[AutoMockData] +public void FixtureInjection(IFixture fixture) +{ + Assert.NotNull(fixture); +} +``` + +## IgnoreVirtualMembers Issue + +You should be aware that the *CLR* requires that interface methods be marked as virtual. Please look at the following example: + +```csharp +public interface IUser +{ + string Name { get; set; } + User Substitute { get; set; } +} + +public class User : IUser +{ + public string Name { get; set; } + public virtual User Substitute { get; set; } +} +``` + +Only `Substitute` property is explicitly marked as `virtual`. In such a situation, *the compiler* marks the remaining properties as `virtual` and `sealed`. Consequently [AutoFixture](https://github.com/AutoFixture/AutoFixture) assigns `null` value to those properties when the `IgnoreVirtualMembers` option is set to `true`. + +```csharp +[Theory] +[AutoMockData(IgnoreVirtualMembers = true)] +public void IssueWithClassThatImplementsInterface(User user) +{ + Assert.Null(user.Name); + Assert.Null(user.Substitute); +} +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..6b713898 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,23 @@ +site_name: AutoFixture.XUnit2.AutoMock Documentation +site_url: https://accenture.github.io/AutoFixture.XUnit2.AutoMock/ +theme: + name: material +nav: + - Home: index.md + - Supported Mocking Libraries: supported-mocking-libraries.md + - Attributes: + - Overview: attributes/index.md + - Test Method Attributes: + - AutoMockData: attributes/auto-mock-data-attribute.md + - InlineAutoMockData: attributes/inline-auto-mock-data-attribute.md + - MemberAutoMockData: attributes/member-auto-mock-data-attribute.md + - Parameter Configuration Attributes: + - IgnoreVirtualMembers: attributes/ignore-virtual-members-attribute.md + - CustomizeWith: attributes/customize-with-attribute.md + - CustomizeWith: attributes/customize-with-t-attribute.md + - Data Filtering Attributes: + - Except: attributes/except-attribute.md + - PickFromRange: attributes/pick-from-range-attribute.md + - PickNegative: attributes/pick-negative-attribute.md + - PickFromValues: attributes/pick-from-values-attribute.md + - Tips and Tricks: tips-and-tricks.md \ No newline at end of file