Skip to content

Commit

Permalink
(Unity) Attribute-based BDD-style tests (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
sbergen committed Apr 12, 2022
1 parent 21eed71 commit cfaba20
Show file tree
Hide file tree
Showing 15 changed files with 461 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and thus not under semantic versioning.
### Added
- New `GroupedAs` operator to allow more control over the state strings of compound operations.
- Experimental: New BDD-style keywords to build scenarios.
- Experimental, Unity: `Feature` and `Scenario` attributes for leaner BDD-style tests

## [4.2.0] - 2021-10-13

Expand Down
58 changes: 58 additions & 0 deletions ResponsibleUnity/Assets/UnityTests/BddTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using NUnit.Framework;
using Responsible.Bdd;
using static Responsible.Bdd.Keywords;
using static Responsible.Responsibly;

namespace Responsible.UnityTests
{
[Feature("BDD-style test")]
public class BddTests : BddTest
{
private bool setUpCalled;
private bool givenExecuted;
private bool whenExecuted;
private bool tearDownCalled;

[SetUp]
public void SetUp()
{
this.setUpCalled = true;
this.givenExecuted = false;
this.whenExecuted = false;
}

[TearDown]
public void TearDown()
{
this.tearDownCalled = true;
}

// It's really hard to know for sure if TearDown gets run with custom attributes,
// However, if SetUp is called, we can be pretty sure TearDown is also called.
// But lets do this just for the heck of it anyway.
// If anyone happens to look at this code and has ideas, please hit me up!
[OneTimeTearDown]
public void OneTimeTearDown()
{
Assert.IsTrue(this.tearDownCalled, "Tear down should have been called at least once");
}

[Scenario("A basic BDD-style test runs without error")]
public IBddStep[] BasicTest() => new[]
{
Given(
"the test is set up properly",
Do("Execute Given", () => this.givenExecuted = true)),
When(
"we execute the when step",
Do("Execute When", () => this.whenExecuted = true)),
Then(
"the state of the test class should be in the final expected state",
Do(
"Assert state",
() => Assert.AreEqual(
(true, true, true),
(this.setUpCalled, this.givenExecuted, this.whenExecuted)))),
};
}
}
3 changes: 3 additions & 0 deletions ResponsibleUnity/Assets/UnityTests/BddTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

135 changes: 135 additions & 0 deletions ResponsibleUnity/Assets/UnityTests/FeatureAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using Responsible.Bdd;

namespace Responsible.UnityTests
{
public class FeatureAttributeTests
{
private class NonBddTest
{
}

private class InvalidReturnTypeBddTest : BddTest
{
[Scenario("Invalid scenario")]
public void InvalidScenario()
{
}
}

private class ValidBddTest : BddTest
{
[Scenario("Test scenario 1")]
public IBddStep[] TestScenario1() => Array.Empty<IBddStep>();

[Scenario("Test scenario 2")]
public IBddStep[] TestScenario2() => Array.Empty<IBddStep>();
}

private class MixedBddTest : BddTest
{
[Scenario("Test scenario")]
public IBddStep[] TestScenario() => Array.Empty<IBddStep>();

[Ignore("Just used for attribute testing")]
[Test]
public void NormalTest()
{
}
}

[Test]
public void BuildingTest_ShouldMarkSuiteAsNotRunnable_WhenClassDoesNotInheritBddTest()
{
var suite = BuildSuites<NonBddTest>().Single();
AssertTestNotRunnableWithReasonContaining(suite, "must inherit from BddTest");
}

[Test]
public void BuildingTest_ShouldMarkScenarioAsNotRunnable_WhenReturnTypeIsIncorrect()
{
var test = BuildSuites<InvalidReturnTypeBddTest>()
.Single()
.Tests
.Single();
AssertTestNotRunnableWithReasonContaining(
test,
"Scenario return type must be convertible to IEnumerable<IBddStep>, got System.Void");
}

[Test]
public void BuildingTest_ShouldHaveGivenName_WhenClassIsSetUpCorrectly()
{
var suite = BuildSuites<ValidBddTest>().Single();
Assert.AreEqual("Feature: Test feature", suite.Name);
}

[Test]
public void BuildingTest_ShouldContainAllScenarios_WhenClassIsSetUpCorrectly()
{
var testNames = BuildSuites<ValidBddTest>()
.Single()
.Tests.Select(t => t.Name)
.ToArray();

CollectionAssert.AreEquivalent(
new[] { "Scenario: Test scenario 1", "Scenario: Test scenario 2" },
testNames);
}

[Test]
public void BuildingTests_ShouldReturnTwoSuites_WhenMixingStyles()
{
var suiteNames = BuildSuites<MixedBddTest>()
.Select(s => s.Name)
.ToArray();

CollectionAssert.AreEquivalent(
new[]
{
"Feature: Test feature",
$"{nameof(FeatureAttributeTests)}+{nameof(MixedBddTest)}"
},
suiteNames);
}

[Test]
public void BuildingTests_ShouldReturnAllTests_WhenMixingStyles()
{
var testNames = BuildSuites<MixedBddTest>()
.SelectMany(suite => suite.Tests)
.Select(test => test.Name)
.ToArray();

CollectionAssert.AreEquivalent(
new[]
{
"Scenario: Test scenario",
"NormalTest",
},
testNames);
}

private static IEnumerable<TestSuite> BuildSuites<T>()
{
var fakeAttribute = new FeatureAttribute("Test feature");
var fixtureBuilder = (IFixtureBuilder)fakeAttribute;
return fixtureBuilder.BuildFrom(new TypeWrapper(typeof(T)));
}

[AssertionMethod]
private static void AssertTestNotRunnableWithReasonContaining(ITest test, string reason)
{
Assert.AreEqual(RunState.NotRunnable, test.RunState);
StringAssert.Contains(
reason,
(string)test.Properties.Get(PropertyNames.SkipReason));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Shared.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_STATEMENT_CONDITIONS/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_DECLARATION_LPAR/@EntryValue">True</s:Boolean>
Expand Down
71 changes: 71 additions & 0 deletions com.beatwaves.responsible/Runtime/Bdd/BddTest.Unity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using Responsible.Unity;
using UnityEngine.TestTools;
using static Responsible.Bdd.Keywords;

namespace Responsible.Bdd
{
/// <summary>
/// Base class for BDD-style tests using attributes.
/// </summary>
/// <remarks>
/// This is Unity-only, because we depend on NUnit in the attributes,
/// and don't depend on NUnit for the vanilla C# version of Responsible.
/// A non-Unity version might be implemented later.
/// </remarks>
/// <seealso cref="FeatureAttribute"/>
/// <seealso cref="ScenarioAttribute"/>
public abstract class BddTest
{
/// <summary>
/// A test instruction executor that is automatically set up and torn down between tests.
/// See <see cref="MakeExecutor"/> for customization options.
/// </summary>
protected TestInstructionExecutor Executor { get; private set; }

/// <summary>
/// Set-up method for NUnit, should not be used manually.
/// </summary>
[SetUp]
public void BddTestSetUp() => this.Executor = this.MakeExecutor();

/// <summary>
/// Tear-down method for NUnit, should not be used manually.
/// </summary>
[TearDown]
public void BddTestTearDown() => this.Executor.Dispose();

/// <summary>
/// Helper method for executing BDD steps, should not be used manually,
/// but must be public to make NUnit happy.
/// </summary>
/// <param name="scenario">Name of the test scenario.</param>
/// <param name="testMethod">Test method that returns the test steps.</param>
/// <returns></returns>
public IEnumerator ExecuteScenario(string scenario, IMethodInfo testMethod)
{
var steps = (IEnumerable<IBddStep>)testMethod.Invoke(this);
return Scenario(scenario).WithSteps(steps.ToArray()).ToYieldInstruction(this.Executor);
}

/// <summary>
/// Creates a test instruction executor for a test run. May be customized in deriving classes.
/// </summary>
/// <returns>A new test instruction executor to be used for the next test.</returns>
[PublicAPI]
protected virtual TestInstructionExecutor MakeExecutor() => new UnityTestInstructionExecutor();

/// <summary>
/// Gets the scenario execution method for a deriving type.
/// The exact type matters, because NUnit will use it to resolve set-up and tear-down methods.
/// </summary>
internal static IMethodInfo GetExecuteScenarioMethod(ITypeInfo derivingType) =>
new MethodWrapper(derivingType.Type, nameof(ExecuteScenario));
}
}
3 changes: 3 additions & 0 deletions com.beatwaves.responsible/Runtime/Bdd/BddTest.Unity.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit cfaba20

Please sign in to comment.