34

Xunit 1.9.x provides the user with the DynamicSkipExample.cs example to help him setting up dynamic skipping of a [Fact].

This has proven to be quite useful when performing some cross-platform development. This allows a test to be temporarily ignored when it cannot be properly run because of the underlying context (OS, filesystem, ...).

However, this example has been dropped in commit 2deeff5 on the road to version 2.0.

How can one re-implement such a functionality through one of the extensibility points of Xunit 2.0?

Note: An issue about this topic has been raised in the xUnit tracker. See xunit/xunit#250.

nulltoken
  • 64,429
  • 20
  • 138
  • 130

3 Answers3

47

[Update: XUnit v2.0 (RTM) is now available and skippable tests are supported by it directly. Use [Fact (Skip = "specific reason")] ]

Note that XUnit v2.0 has not shipped. This example is compatible with Xunit 2.0 beta5 which you can find on nuget.org. There may be other ways to accomplish this (as this is just the example I came to).

1) Define an attribute that will decorate your tests.

/// <inheritdoc/>
[AttributeUsage( AttributeTargets.Method, AllowMultiple = false )]
[XunitTestCaseDiscoverer( "SkippableTestCaseDiscoverer", "assemblynamehere" )]
public sealed class SkippableTestAttribute : FactAttribute
{
    public SkippableTestAttribute() { }
}    

2) Create your discoverer. (We followed the code example at https://github.com/xunit/xunit/blob/2d9ce6fbd75e91a69a0cc83e1bc3d4eab18b2c6c/src/xunit.execution/Sdk/Frameworks/TheoryDiscoverer.cs)

/// <summary>
/// Implementation of <see cref="IXunitTestCaseDiscoverer"/> that supports finding test cases
/// on methods decorated with <see cref="SkippableTestAttribute"/>.
/// </summary>
public class SkippableTestCaseDiscoverer : IXunitTestCaseDiscoverer
{
    /// <inheritdoc/>
    [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Needs to return test case." )]
    public IEnumerable<IXunitTestCase> Discover( ITestMethod testMethod, IAttributeInfo factAttribute )
    {
        // some of this code is from https://github.com/xunit/xunit/blob/2d9ce6fbd75e91a69a0cc83e1bc3d4eab18b2c6c/src/xunit.execution/Sdk/Frameworks/TheoryDiscoverer.cs
        if ( factAttribute.GetNamedArgument<string>( "Skip" ) != null )
            return new[] { new XunitTestCase( testMethod ) };

        var dataAttributes = testMethod.Method.GetCustomAttributes( typeof( DataAttribute ) );

        try
        {
            var results = new List<XunitTestCase>();

            foreach ( var dataAttribute in dataAttributes )
            {
                var discovererAttribute = dataAttribute.GetCustomAttributes( typeof( DataDiscovererAttribute ) ).First();
                var discoverer = ExtensibilityPointFactory.GetDataDiscoverer( discovererAttribute );
                if ( !discoverer.SupportsDiscoveryEnumeration( dataAttribute, testMethod.Method ) )
                    return new XunitTestCase[] { new XunitTheoryTestCase( testMethod ) };

// These lines are our "custom dynamic logic" that determines if we should skip the test.
                IEnumerable<object[]> data = discoverer.GetData( dataAttribute, testMethod.Method );

                if ( data == null )
                {
                    var test = new SkippableTestCase( testMethod );
                    test.SkipTest( "Test not configured with any " );
                    return new[] { test };
                }
                foreach ( var dataRow in data )
                {
                    // Attempt to serialize the test case, since we need a way to uniquely identify a test
                    // and serialization is the best way to do that. If it's not serializable, this will
                    // throw and we will fall back to a single theory test case that gets its data
                    // at runtime.
                    var testCase = new XunitTestCase( testMethod, dataRow );
                    SerializationHelper.Serialize( testCase );
                    results.Add( testCase );
                }
            }

            if ( results.Count == 0 )
                results.Add( new LambdaTestCase( testMethod,
                                               () => { throw new InvalidOperationException( String.Format( "No data found for {0}.{1}", testMethod.TestClass.Class.Name, testMethod.Method.Name ) ); } ) );

            return results;
        }
        catch
        {
            return new XunitTestCase[] { new XunitTheoryTestCase( testMethod ) };
        }
    }
}

3) Create a class which implements IXunitTestCase (as the default base class doesn't allow modifying the skip reason).

// Class is similar to XunitTestCase 
[Serializable]
public class SkippableTestCase : TestMethodTestCase, IXunitTestCase 
{
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Obsolete("Called by the de-serializer", error: true)]
    public SkippableTestCase() { }

    /// <summary>
    /// Initializes a new instance of the <see cref="SkippableTestCase"/> class.
    /// </summary>
    /// <param name="testMethod">The test method this test case belongs to.</param>
    /// <param name="testMethodArguments">The arguments for the test method.</param>
    public SkippableTestCase(ITestMethod testMethod, object[] testMethodArguments = null)
        : base(testMethod, testMethodArguments) { }

    /// <inheritdoc />
    protected SkippableTestCase(SerializationInfo info, StreamingContext context)
        : base(info, context) { }

    public void SkipTest( string reason )
    {
        base.SkipReason = reason;
    }

    /// <inheritdoc/>
    public virtual Task<RunSummary> RunAsync( IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource )
    {
        return new XunitTestCaseRunner( this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource ).RunAsync();
    }
}

You have many options for how you want to set the base.SkipReason. In this sample, a public method was created.

This example will skip tests that have a MemberDataAttribute that returns no data rows. You can modify it to return the SkippableTestCase based on your criteria. For example, this discover skips tests on Sunday.

/// <inheritdoc/>
[System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Needs to return test case." )]
public IEnumerable<IXunitTestCase> Discover( ITestMethod testMethod, IAttributeInfo factAttribute )
{
    if ( DateTime.Today.DayOfWeek == DayOfWeek.Sunday )
    {
        var test = new SkippableTestCase( testMethod );
        test.SkipTest( "Test not configured with any " );
        return new[] { test };
    }
    else
    {
        return new[] { new XunitTestCase( testMethod ) };
    }
}
vcsjones
  • 138,677
  • 31
  • 291
  • 286
Matthew K
  • 973
  • 6
  • 21
  • 1
    Thanks for this. One of the great things of the initial implementation was that it was letting the test "decide" to be skipped by throwing a SkipException. Could you please update your answer to demonstrate how to do such a thing? Indeed, in many cases, the discoverer may not have all the data to make the decision to skip or not. – nulltoken Jan 08 '15 at 06:58
  • Allowing tests to be skipped by catching a SkipException requires rolling a custom runner. Would you like me to demonstrate such code? You would then be unable to run your tests with any of the default runners (Console, VS, etc). – Matthew K Jan 10 '15 at 16:17
  • By taking a further look at TestRunner.RunAsync() it seems that you're indeed correct and that there's no way with version beta5-2785 to "skip" the test mid-flight. As such, I'm going to accept you answer. Thanks a lot for all your explanations! – nulltoken Jan 12 '15 at 08:33
  • Just took a look at your latest update (*"skippable tests are supported by it directly"*). Would you be so kind as to add a link to an example, or documentation about this? – nulltoken Apr 23 '15 at 08:51
9

You can also search for SkippableFact in the Nugget Manager, then you can use all functions with [SkippableFact]

Dominik Lemberger
  • 2,287
  • 2
  • 21
  • 33
3

This has eventually been fixed in issue xunit/samples.xunit#8.

The amazing @BradWilson made this available as a complete and detailed sample in the samples.xunit repository.

nulltoken
  • 64,429
  • 20
  • 138
  • 130