Search code examples
.netmstest

Repeat mstest test run multiple times


Some of my mstest unit tests help detect multi-threading race conditions, and as such they are most useful when run many times in a row, but I only want to do this for specific test runs -- not all the time.

Is there a way to configure mstest (in the Test List Editor preferably) to run a test multiple times?


Solution

  • I needed to do something similar, so I came up with a solution to this.

    It's not simple, but once everything is setup you can reuse it across projects. I also have a download of this code on GitHub (https://github.com/johnkoerner/MSTestLooper), but in case that goes away at some point, here is how I did it.

    First we create an attribute that we will apply to our class to tell it run all the tests multiple times. Do all of this in a separate assembly, because the DLL needs to live in a special location.

    [Serializable]
    public class TestLooperAttribute :  TestClassExtensionAttribute
    {
        private static readonly Uri thisGuy = new Uri("urn:TestLooperAttribute");
    
        private string _PropertyName;
        public string PropertyName
        {
            get
            { return _PropertyName; }
            set
            {
                _PropertyName = value;
            }
        }
        public override Uri ExtensionId
        {
    
            get {
                return thisGuy; }
        }
    
    
            public override TestExtensionExecution GetExecution()
        {
    
            return new TestLooperExecution(PropertyName);
        }
    }
    

    Next we have to create a custom test class execution class:

    class TestLooperExecution : TestExtensionExecution
    {
        private string PropertyName;
    
        public TestLooperExecution(string PropertyName)
        {
            this.PropertyName = PropertyName;
        }
    
        public override ITestMethodInvoker CreateTestMethodInvoker(TestMethodInvokerContext InvokerContext)
        {
            return new TestLooperInvoker(InvokerContext, PropertyName);
        }
    
        public override void Dispose()
        {
            //TODO: Free, release or reset native resources
        }
    
        public override void Initialize(TestExecution Execution)
        {
            //TODO: Wire up event handlers for test events if needed
    
        }
    }
    

    Finally we add a custom invoker, which is where we perform the looping:

    class TestLooperInvoker : ITestMethodInvoker
    {
        private TestMethodInvokerContext m_invokerContext;
        private string PropertyName;
    
        public TestLooperInvoker(TestMethodInvokerContext InvokerContext, string PropertyName)
        {
            m_invokerContext = InvokerContext;
            this.PropertyName = PropertyName;
        }
    
        public TestMethodInvokerResult Invoke(params object[] args)
        {
    
            // Our helper results class to aggregate our test results
            HelperTestResults results = new HelperTestResults();
    
            IEnumerable<object> objects = m_invokerContext.TestContext.Properties[PropertyName] as IEnumerable<object>;
    
            foreach (var d in objects)
                results.AddTestResult(m_invokerContext.InnerInvoker.Invoke(d), new object[1] { d.GetType().ToString()});
    
            var output = results.GetAllResults();
            m_invokerContext.TestContext.WriteLine(output.ExtensionResult.ToString());
    
            return output;
        }
    }
    

    The HelperTestResults class just builds up strings for output, you can handle this how you want and I don't want to include that code because it will just make this post that much longer.

    Compile this into a DLL and then you need to copy it to

    C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\PublicAssemblies
    

    You also have to create a registry entry for the class:

    Windows Registry Editor Version 5.00 
    [HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\11.0\EnterpriseTools\QualityTools\TestTypes\{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}\TestTypeExtensions\TestLooperAttribute]
    "AttributeProvider"="TestLooper.TestLooperAttribute, TestLooper"
    

    Now that you have all of that done, you can finally use the class:

    using System;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using TestLooper;
    using System.Collections.Generic;
    namespace UnitTestSamples
    {
        [TestLooper(PropertyName="strings")]
        public class UnitTest1
        {
            public static List<String> strings = new List<String>();
            private TestContext testContextInstance;
    
            public TestContext TestContext
            {
                get
                {
                    return testContextInstance;
                }
                set
                {
                    testContextInstance = value;
                }
            }
            [ClassInitialize()]
            public static void Init(TestContext x)
            {
                strings.Add("A");
                strings.Add("B");
                strings.Add("C");
                strings.Add("D");
    
            }
    
            [TestInitialize()]
            public void TestInit()
            {
                if (!TestContext.Properties.Contains("strings"))
                testContextInstance.Properties.Add("strings", strings);
            }
    
            [TestMethod]
            [DataSource("Microsoft.VisualStudio.TestTools.DataSource.CSV", "DataDriven1.csv", "DataDriven1#csv", DataAccessMethod.Sequential)]
            [DeploymentItem("DataDriven1.csv")]
            public void TestMethodStrings(string s)
    
            {
                int value1 = Convert.ToInt32(TestContext.DataRow["Col1"]); ;
                TestContext.WriteLine(String.Format("{0}:{1}", s, value1));
            }
        }
    }
    

    Notice that our test method accepts a parameter, which comes from the test looper. I also show this using a data driven test, to show you can combine the two together to generate large permutations across your data sets.