diff --git a/appveyor.yml b/appveyor.yml index be0c26c..4947040 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ skip_branch_with_pr: true max_jobs: 1 configuration: Release -image: Visual Studio 2017 +image: Visual Studio 2019 init: - git config --global core.autocrlf input @@ -29,8 +29,6 @@ test: off test_script: - ps: dotnet test --no-build - - ps: dotnet test Tests/Snapper.Nunit.Tests/Snapper.Nunit.Tests.csproj --logger:Appveyor --no-build - - ps: dotnet test Tests/Snapper.TestFrameworkSupport.Tests/Snapper.TestFrameworkSupport.Tests.csproj --logger:Appveyor --no-build deploy: - provider: NuGet diff --git a/docs/pages/quickstart.md b/docs/pages/quickstart.md index 7e5f689..cc3ec87 100644 --- a/docs/pages/quickstart.md +++ b/docs/pages/quickstart.md @@ -14,12 +14,13 @@ To run this quickstart, you need the following prerequisites: ```bash nuget install Snapper ``` -2. Create an xUnit test like shown +1. Create an xUnit test like shown ```csharp public class MyTestClass { [Fact] - public void MyTest(){ + public void MyTest() + { var obj = new { Key = "value" }; @@ -28,42 +29,26 @@ public class MyTestClass { } } ``` +Run the test and you'll see that it passes. The above code will try match the `obj` variable with a snapshot in the file `_snapshots/MyTestClass_MyTest.json` (relative to the file in which `MyTestClass` exists). -For a new test this will fail as the file does not exist with an error similiar to this. -``` -Snapper.Exceptions.SnapshotDoesNotExistException : A snapshot does not exist. -Apply the [UpdateSnapshots] attribute on the test method or class and then run the test again to create a snapshot. -``` -3. Lets create the snapshot so that the test passes. We could create the snapshot file manually if we wanted but that's a bit annoying. Luckily Snapper can generate a snapshot file for us! Apply the `[UpdateSnapshots]` attribute on the method and run the test again. -```csharp -public class MyTestClass { - - [UpdateSnapshots] - [Fact] - public void MyTest(){ - var obj = new { - Key = "value" - }; - - obj.ShouldMatchSnapshot(); - } -} -``` +For a new test the snapshot file would not exist yet so Snapper automatically creates it on the first run. +> Snapper automatically creates a new snapshot file for a new test from version v2.3.0 onwards. For previous versions use the `[UpdateSnapshots]` attribute to generate your initial snapshot. -4. A file called `_snapshots/MyTestClass_MyTest.json` should have been created with the following content. +1. A file called `_snapshots/MyTestClass_MyTest.json` should have been created with the following content. ```json { "Key": "value" } ``` -You can now remove the `[UpdateSnapshots]` attribute on the method. You should also commit the `_snapshots/MyTestClass_MyTest.json` snapshot file with your source code. +You should commit the `_snapshots/MyTestClass_MyTest.json` snapshot file with your source code. -5. Lets make our test fail. Update your code to the following. +1. Lets make our test fail due to a change in requirements. Update your code to the following. ```csharp public class MyTestClass { [Fact] - public void MyTest(){ + public void MyTest() + { var obj = new { Key = "My new value" }; @@ -84,6 +69,23 @@ Run the test and you will see a nice error message showing the difference betwee + "Key": "My new value" } ``` -You can then update the snapshot by adding the `[UpdateSnapshots]` attribute to the test and running it again or if the change was invalid fix the failing test. + +1. Once you've verified the new snapshot is expected you can update the snapshot by appling the `[UpdateSnapshots]` attribute on the method and then running the test again. +```csharp +public class MyTestClass { + + [UpdateSnapshots] + [Fact] + public void MyTest() + { + var obj = new { + Key = "My new value" + }; + + obj.ShouldMatchSnapshot(); + } +} +``` +This will update the snapshot file with the latest snapshot. Remember to remove the `[UpdateSnapshots]` attribute before you commit your code!

For more examples of tests written using Snapper see [here](https://github.com/theramis/Snapper/tree/master/project/Tests/Snapper.Tests). \ No newline at end of file diff --git a/project/Snapper/Attributes/UpdateSnapshotsAttribute.cs b/project/Snapper/Attributes/UpdateSnapshotsAttribute.cs index b4835be..b01ea25 100644 --- a/project/Snapper/Attributes/UpdateSnapshotsAttribute.cs +++ b/project/Snapper/Attributes/UpdateSnapshotsAttribute.cs @@ -1,11 +1,11 @@ -using System; +using System; namespace Snapper.Attributes { /// /// /// Tells Snapper to update snapshots. Can be placed on methods, classes and assembly. - /// Note: Has no effect when on inline snapshots + /// Note: Has no effect when using inline snapshots /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] public class UpdateSnapshotsAttribute : Attribute diff --git a/project/Snapper/Core/CIEnvironmentDetector.cs b/project/Snapper/Core/CIEnvironmentDetector.cs new file mode 100644 index 0000000..0940dd6 --- /dev/null +++ b/project/Snapper/Core/CIEnvironmentDetector.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Snapper.Core +{ + internal static class CiEnvironmentDetector + { + /// + /// Based on https://github.com/watson/ci-info/blob/2012259979fc38517f8e3fc74daff714251b554d/index.js#L52-L59 + /// + private static IEnumerable CIEnvironmentVariables = new List + { + "CI", // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari + "CONTINUOUS_INTEGRATION", // Travis CI, Cirrus CI + "BUILD_NUMBER", // Jenkins, TeamCity + "BUILD_BUILDNUMBER", // Azure DevOps + "RUN_ID" // TaskCluster, dsari + }; + + public static bool IsCiEnv() + { + foreach (var envVarTarget in new[] { EnvironmentVariableTarget.Process, EnvironmentVariableTarget.Machine, EnvironmentVariableTarget.User }) + { + var found = CIEnvironmentVariables.Any(ciEnvironmentVariable => + Environment.GetEnvironmentVariable(ciEnvironmentVariable, envVarTarget) != null); + if (found) + { + return true; + } + } + + return false; + } + } +} diff --git a/project/Snapper/Core/Messages.cs b/project/Snapper/Core/Messages.cs index d840552..7fa40ce 100644 --- a/project/Snapper/Core/Messages.cs +++ b/project/Snapper/Core/Messages.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Text; -using Snapper.Attributes; using Snapper.Json; namespace Snapper.Core @@ -12,12 +11,9 @@ public static string GetSnapResultMessage(SnapResult result) switch (result.Status) { case SnapResultStatus.SnapshotDoesNotExist: - var attributeName = nameof(UpdateSnapshotsAttribute).Replace("Attribute", string.Empty); var message = new StringBuilder(); message.AppendLine($"A snapshot does not exist.{Environment.NewLine}"); - message.AppendLine($"Apply the [{attributeName}] attribute on the " + - $"test method or class and then run the test again to " + - $"create a snapshot."); + message.AppendLine("Run the test outside of a CI environment to create a snapshot."); return message.ToString(); case SnapResultStatus.SnapshotsMatch: return "Snapshots Match."; diff --git a/project/Snapper/Core/SnapperCore.cs b/project/Snapper/Core/SnapperCore.cs index 2e53c98..82422ee 100644 --- a/project/Snapper/Core/SnapperCore.cs +++ b/project/Snapper/Core/SnapperCore.cs @@ -17,10 +17,8 @@ internal class SnapperCore protected SnapResult Snap(SnapshotId snapshotId, object newSnapshot) { var currentSnapshot = _snapshotStore.GetSnapshot(snapshotId); - var areSnapshotsEqual = currentSnapshot != null - && _snapshotComparer.CompareSnapshots(currentSnapshot, newSnapshot); - - if (!areSnapshotsEqual && _snapshotUpdateDecider.ShouldUpdateSnapshot()) + + if (ShouldUpdateSnapshot(currentSnapshot, newSnapshot)) { _snapshotStore.StoreSnapshot(snapshotId, newSnapshot); return SnapResult.SnapshotUpdated(currentSnapshot, newSnapshot); @@ -31,9 +29,27 @@ protected SnapResult Snap(SnapshotId snapshotId, object newSnapshot) return SnapResult.SnapshotDoesNotExist(newSnapshot); } - return areSnapshotsEqual + return _snapshotComparer.CompareSnapshots(currentSnapshot, newSnapshot) ? SnapResult.SnapshotsMatch(currentSnapshot, newSnapshot) : SnapResult.SnapshotsDoNotMatch(currentSnapshot, newSnapshot); } + + private bool ShouldUpdateSnapshot(object currentSnapshot, object newSnapshot) + { + var snapshotsAreEqual = currentSnapshot != null + && _snapshotComparer.CompareSnapshots(currentSnapshot, newSnapshot); + if (!snapshotsAreEqual && _snapshotUpdateDecider.ShouldUpdateSnapshot()) + { + return true; + } + + // Create snapshot if it doesn't currently exist and its not a CI env + if (currentSnapshot == null) + { + return !CiEnvironmentDetector.IsCiEnv(); + } + + return false; + } } } diff --git a/project/Snapper/Core/SnapshotUpdateDecider.cs b/project/Snapper/Core/SnapshotUpdateDecider.cs index d18f5dc..2da371b 100644 --- a/project/Snapper/Core/SnapshotUpdateDecider.cs +++ b/project/Snapper/Core/SnapshotUpdateDecider.cs @@ -13,18 +13,6 @@ internal class SnapshotUpdateDecider : ISnapshotUpdateDecider private const string UpdateSnapshotEnvironmentVariableName = "UpdateSnapshots"; private readonly string _envVarName; - /// - /// Based on https://github.com/watson/ci-info/blob/2012259979fc38517f8e3fc74daff714251b554d/index.js#L52-L59 - /// - private readonly IEnumerable _ciEnvironmentVariables = new List - { - "CI", // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari - "CONTINUOUS_INTEGRATION", // Travis CI, Cirrus CI - "BUILD_NUMBER", // Jenkins, TeamCity - "BUILD_BUILDNUMBER", // Azure DevOps - "RUN_ID" // TaskCluster, dsari - }; - public SnapshotUpdateDecider(ITestMethodResolver testMethodResolver, string envVarName = UpdateSnapshotEnvironmentVariableName) { @@ -59,22 +47,7 @@ private bool ShouldUpdateSnapshotBasedOnAttribute() { if (TryGetUpdateSnapshotsAttribute(customAttributeProvider, out var att)) { - return !(att.IgnoreIfCi && IsCiEnv()); - } - } - - return false; - } - - private bool IsCiEnv() - { - foreach (var envVarTarget in new[] { EnvironmentVariableTarget.Process, EnvironmentVariableTarget.Machine, EnvironmentVariableTarget.User}) - { - var found = _ciEnvironmentVariables.Any(ciEnvironmentVariable => - Environment.GetEnvironmentVariable(ciEnvironmentVariable, envVarTarget) != null); - if (found) - { - return true; + return !(att.IgnoreIfCi && CiEnvironmentDetector.IsCiEnv()); } } diff --git a/project/Tests/Snapper.Internals.Tests/Core/SnapperCoreTests.cs b/project/Tests/Snapper.Internals.Tests/Core/SnapperCoreTests.cs index 6dc5c50..6ca7ff1 100644 --- a/project/Tests/Snapper.Internals.Tests/Core/SnapperCoreTests.cs +++ b/project/Tests/Snapper.Internals.Tests/Core/SnapperCoreTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System; +using FluentAssertions; using Moq; using Snapper.Core; using Xunit; @@ -117,8 +118,26 @@ public void SnapshotDoesNotExist_ShouldUpdate_ResultStatusIs_SnapshotUpdated() } [Fact] - public void SnapshotDoesNotExist_ResultStatusIs_SnapshotDoesNotExist() + public void SnapshotDoesNotExist_ResultStatusIs_SnapshotUpdated() { + // Tests run on CI so clearing the CI environment variable to emulate local machine + Environment.SetEnvironmentVariable("CI", null, EnvironmentVariableTarget.Process); + + _store.Setup(a => a.GetSnapshot(It.IsAny())).Returns(null); + _updateDecider.Setup(a => a.ShouldUpdateSnapshot()).Returns(false); + + var result = _snapper.Snap(new SnapshotId("name", null, null, null), _obj); + + _store.Verify(a => a.StoreSnapshot(It.IsAny(), It.IsAny()), Times.Once); + result.Status.Should().BeEquivalentTo(SnapResultStatus.SnapshotUpdated); + result.OldSnapshot.Should().BeNull(); + result.NewSnapshot.Should().BeEquivalentTo(_obj); + } + + [Fact] + public void SnapshotDoesNotExist_And_IsCiEnv_ResultStatusIs_SnapshotDoesNotExist() + { + Environment.SetEnvironmentVariable("CI", "true", EnvironmentVariableTarget.Process); _store.Setup(a => a.GetSnapshot(It.IsAny())).Returns(null); _updateDecider.Setup(a => a.ShouldUpdateSnapshot()).Returns(false); @@ -127,6 +146,7 @@ public void SnapshotDoesNotExist_ResultStatusIs_SnapshotDoesNotExist() result.Status.Should().BeEquivalentTo(SnapResultStatus.SnapshotDoesNotExist); result.OldSnapshot.Should().BeNull(); result.NewSnapshot.Should().BeEquivalentTo(_obj); + Environment.SetEnvironmentVariable("CI", null, EnvironmentVariableTarget.Process); } } } diff --git a/project/Tests/Snapper.Internals.Tests/Core/SnapshotUpdateDeciderTests.cs b/project/Tests/Snapper.Internals.Tests/Core/SnapshotUpdateDeciderTests.cs index 16097c8..11ffaeb 100644 --- a/project/Tests/Snapper.Internals.Tests/Core/SnapshotUpdateDeciderTests.cs +++ b/project/Tests/Snapper.Internals.Tests/Core/SnapshotUpdateDeciderTests.cs @@ -33,6 +33,7 @@ public void TrueEnvironmentVariableSet_ShouldUpdate(string value) { Environment.SetEnvironmentVariable(_envVar, value, EnvironmentVariableTarget.Process); _decider.ShouldUpdateSnapshot().Should().BeTrue(); + Environment.SetEnvironmentVariable(_envVar, null, EnvironmentVariableTarget.Process); } [Theory] @@ -44,6 +45,7 @@ public void FalseEnvironmentVariableSet_ShouldNotUpdate(string value) { Environment.SetEnvironmentVariable(_envVar, value, EnvironmentVariableTarget.Process); _decider.ShouldUpdateSnapshot().Should().BeFalse(); + Environment.SetEnvironmentVariable(_envVar, null, EnvironmentVariableTarget.Process); } [Theory] @@ -54,6 +56,7 @@ public void InvalidEnvironmentVariableSet_ShouldNotUpdate(string value) { Environment.SetEnvironmentVariable(_envVar, value, EnvironmentVariableTarget.Process); _decider.ShouldUpdateSnapshot().Should().BeFalse(); + Environment.SetEnvironmentVariable(_envVar, null, EnvironmentVariableTarget.Process); } [Fact] @@ -69,6 +72,7 @@ public void UpdateAttribute_TrueEnvironmentVariableSet_ShouldUpdate() { Environment.SetEnvironmentVariable(_envVar, "true", EnvironmentVariableTarget.Process); _decider.ShouldUpdateSnapshot().Should().BeTrue(); + Environment.SetEnvironmentVariable(_envVar, null, EnvironmentVariableTarget.Process); } [Fact] @@ -77,6 +81,7 @@ public void UpdateAttribute_FalseEnvironmentVariableSet_ShouldUpdate() { Environment.SetEnvironmentVariable(_envVar, "False", EnvironmentVariableTarget.Process); _decider.ShouldUpdateSnapshot().Should().BeTrue(); + Environment.SetEnvironmentVariable(_envVar, null, EnvironmentVariableTarget.Process); } } diff --git a/project/Tests/Snapper.Internals.Tests/Snapper.Internals.Tests.csproj b/project/Tests/Snapper.Internals.Tests/Snapper.Internals.Tests.csproj index 8b783a8..421e533 100644 --- a/project/Tests/Snapper.Internals.Tests/Snapper.Internals.Tests.csproj +++ b/project/Tests/Snapper.Internals.Tests/Snapper.Internals.Tests.csproj @@ -1,6 +1,6 @@ - + - netcoreapp2.2;net45 + netcoreapp3.1;net452 false true latest diff --git a/project/Tests/Snapper.Nunit.Tests/Snapper.Nunit.Tests.csproj b/project/Tests/Snapper.Nunit.Tests/Snapper.Nunit.Tests.csproj index 51309c1..cd10b70 100644 --- a/project/Tests/Snapper.Nunit.Tests/Snapper.Nunit.Tests.csproj +++ b/project/Tests/Snapper.Nunit.Tests/Snapper.Nunit.Tests.csproj @@ -1,13 +1,12 @@ - + - netcoreapp2.2;net45 + netcoreapp3.1;net452 false true latest - diff --git a/project/Tests/Snapper.TestFrameworkSupport.Tests/Snapper.TestFrameworkSupport.Tests.csproj b/project/Tests/Snapper.TestFrameworkSupport.Tests/Snapper.TestFrameworkSupport.Tests.csproj index 7019766..9ce128e 100644 --- a/project/Tests/Snapper.TestFrameworkSupport.Tests/Snapper.TestFrameworkSupport.Tests.csproj +++ b/project/Tests/Snapper.TestFrameworkSupport.Tests/Snapper.TestFrameworkSupport.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp2.2;net45 + netcoreapp3.1;net452 false true latest @@ -13,7 +13,6 @@ - diff --git a/project/Tests/Snapper.Tests/Snapper.Tests.csproj b/project/Tests/Snapper.Tests/Snapper.Tests.csproj index a6f7f31..33d6546 100644 --- a/project/Tests/Snapper.Tests/Snapper.Tests.csproj +++ b/project/Tests/Snapper.Tests/Snapper.Tests.csproj @@ -1,6 +1,6 @@ - netcoreapp2.2;net45 + netcoreapp3.1;net452 false true latest diff --git a/project/Tests/Snapper.Tests/SnapperSnapshotsPerClassTests.cs b/project/Tests/Snapper.Tests/SnapperSnapshotsPerClassTests.cs index 5f1a64b..8222f86 100644 --- a/project/Tests/Snapper.Tests/SnapperSnapshotsPerClassTests.cs +++ b/project/Tests/Snapper.Tests/SnapperSnapshotsPerClassTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Runtime.CompilerServices; using Snapper.Attributes; @@ -80,9 +80,10 @@ public void SnapshotsDoNotMatch_UpdateSnapshotsAttributeIsSet_SnapshotIsUpdated( } [Fact] - public void SnapshotDoesNotExist_SnapshotDoesNotExistException_IsThrown() + public void SnapshotDoesNotExist_And_IsCiEnv_SnapshotDoesNotExistException_IsThrown() { // Arrange + Environment.SetEnvironmentVariable("CI", "true", EnvironmentVariableTarget.Process); var snapshot = new { TestValue = "value" @@ -95,9 +96,45 @@ public void SnapshotDoesNotExist_SnapshotDoesNotExistException_IsThrown() Assert.NotNull(exception); Assert.Equal("Snapper.Exceptions.SnapshotDoesNotExistException", exception.GetType().FullName); Assert.Equal( $"A snapshot does not exist.{Environment.NewLine}{Environment.NewLine}" + - "Apply the [UpdateSnapshots] attribute on the " + - "test method or class and then run the test again to " + - $"create a snapshot.{Environment.NewLine}", exception.Message); + $"Run the test outside of a CI environment to create a snapshot.{Environment.NewLine}", exception.Message); + + // Cleanup + Environment.SetEnvironmentVariable("CI", null, EnvironmentVariableTarget.Process); + } + + [Fact] + public void SnapshotsDoesNotExist_SnapshotIsCreated() + { + // Arrange + + // Tests run on CI so clearing the CI environment variable to emulate local machine + Environment.SetEnvironmentVariable("CI", null, EnvironmentVariableTarget.Process); + + var snapshotFilePath = GetSnapshotFilePath(); + + var content = File.ReadAllText(snapshotFilePath); + var snapshotToRemove = string.Join( + Environment.NewLine, + " \"SnapshotsDoesNotExist_SnapshotIsCreated\": {", + " \"TestValue\": \"doesNotExist\"", + " }"); + + var newContent = content.Replace(snapshotToRemove, ""); + File.WriteAllText(snapshotFilePath, newContent); + + var snapshot = new + { + TestValue = "doesNotExist" + }; + + // Act + snapshot.ShouldMatchSnapshot(); + + // Assert + Assert.Contains(snapshotToRemove, File.ReadAllText(snapshotFilePath)); + + // Cleanup + File.WriteAllText(snapshotFilePath, newContent); } [Fact] diff --git a/project/Tests/Snapper.Tests/SnapperSnapshotsPerMethodTests.cs b/project/Tests/Snapper.Tests/SnapperSnapshotsPerMethodTests.cs index fa8c0db..9d2d997 100644 --- a/project/Tests/Snapper.Tests/SnapperSnapshotsPerMethodTests.cs +++ b/project/Tests/Snapper.Tests/SnapperSnapshotsPerMethodTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Runtime.CompilerServices; using Snapper.Attributes; @@ -111,9 +111,10 @@ public void SnapshotsDoNotMatch_UpdateSnapshotsAttributeIsSet_SnapshotIsUpdated( } [Fact] - public void SnapshotDoesNotExist_SnapshotDoesNotExistException_IsThrown() + public void SnapshotDoesNotExist_And_IsCiEnv_SnapshotDoesNotExistException_IsThrown() { // Arrange + Environment.SetEnvironmentVariable("CI", "true", EnvironmentVariableTarget.Process); var snapshot = new { TestValue = "value" @@ -126,9 +127,45 @@ public void SnapshotDoesNotExist_SnapshotDoesNotExistException_IsThrown() Assert.NotNull(exception); Assert.Equal("Snapper.Exceptions.SnapshotDoesNotExistException", exception.GetType().FullName); Assert.Equal( $"A snapshot does not exist.{Environment.NewLine}{Environment.NewLine}" + - "Apply the [UpdateSnapshots] attribute on the " + - "test method or class and then run the test again to " + - $"create a snapshot.{Environment.NewLine}", exception.Message); + $"Run the test outside of a CI environment to create a snapshot.{Environment.NewLine}", exception.Message); + + // Cleanup + Environment.SetEnvironmentVariable("CI", null, EnvironmentVariableTarget.Process); + } + + [Fact] + public void SnapshotsDoesNotExist_SnapshotIsCreated() + { + // Tests run on CI so clearing the CI environment variable to emulate local machine + Environment.SetEnvironmentVariable("CI", null, EnvironmentVariableTarget.Process); + + // Arrange + var snapshotFilePath = GetSnapshotFilePath( + nameof(SnapshotsDoesNotExist_SnapshotIsCreated)); + + if (File.Exists(snapshotFilePath)) + File.Delete(snapshotFilePath); + + var snapshot = new + { + TestValue = "value" + }; + + // Act + snapshot.ShouldMatchSnapshot(); + + // Assert + Assert.True(File.Exists(snapshotFilePath)); + + var expectedSnapshotContent = string.Join( + Environment.NewLine, + "{", + " \"TestValue\": \"value\"", + "}"); + Assert.Equal(expectedSnapshotContent, File.ReadAllText(snapshotFilePath)); + + // Cleanup + File.Delete(snapshotFilePath); } [Fact]