如何为工作流创建自动化测试

How to create an automation test for a workflow

我正在开发一个工作流项目,该项目有 19 个场景用于测试整个系统和 34 个步骤。

所以,我的问题是,如何为它创建自动化测试?

我目前的做法是: 为每个场景创建一个集成测试,然后创建主系统测试以 运行 所有集成测试。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace Project1
{
     // Unit tests
    public class UnitTest_step1
    {
        public void RunTest() { }
    }

    public class UnitTest_step2
    {
        public void RunTest() { }
    }

    public class UnitTest_step3
    {
        public void RunTest() { }
    }

    public class UnitTest_step4
    {
        public void RunTest() { }
    }
     // End of unit tests

    public class IntegrationTests
    {

        public void IntegrationTest1()
        {
            UnitTest_step1.RunTest();
            UnitTest_step2.RunTest();
            UnitTest_step4.RunTest();
        }

        public void IntegrationTest2()
        {
            UnitTest_step1.RunTest();
            UnitTest_step2.RunTest();
            UnitTest_step3.RunTest();
            UnitTest_step4.RunTest();
        }

        public void IntegrationTest3()
        {
            UnitTest_step1.RunTest();
            UnitTest_step4.RunTest();
        }

    }



    [TestClass]
    public class SystemTests
    {
        [TestMethod]
        public void Scenario1()
        {
            IntegrationTests.IntegrationTest1()
        }

        [TestMethod]
        public void Scenario2()
        {
            IntegrationTests.IntegrationTest2();
        }

        [TestMethod]
        public void Scenario3()
        {
            IntegrationTests.IntegrationTest3();
        }

        [TestMethod]
        public void ScenarioN()
        {
            IntegrationTests.IntegrationTestN();
        }
    }
}

此致。

嗯,在我看来,你的问题中提供的信息非常抽象,问题有点过于宽泛。 答案取决于您的工作流引擎是如何实现的以及您的系统要求是什么。 需求和实施细节决定了您的测试方法。

我首先要澄清你有什么样的步骤,是否传递了任何数据上下文, 这些步骤会产生什么副作用(将数据写入数据库、发送事件、调用其他系统 API 等), 做步骤相互依赖等等。

另一个问题是您需要如何断言结果,在每个步骤之后或场景之后? 系统应该是可测试的,通常,每个步骤都应该包含单元测试。 因此,建议的假设方法是用独立的单元测试覆盖每个步骤 和集成测试场景。

我想出了一个简单的例子来说明其中一种通用方法。 为简单起见,我假设步骤很少或没有数据上下文并且可以重新排序。

namespace Workflow.Test
{
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using System;
    using System.Collections.Generic;

    [TestClass]
    public class SystemTests
    {
        [TestMethod]
        public void Scenario1()
        {
            new Workflow().Run(new Scenario1());
        }

        [TestMethod]
        public void Scenario2()
        {
            new Workflow().Run(new Scenario2());
        }

        // The advantage of explicit steps declaration is test readability.
        // Declarative approach also enables the further possibility of test generation!
        [TestMethod]
        public void MoreExplicitAndDeclarative()
        {
            new Workflow().Run(new List<Type>
            {
                typeof(Step1),
                typeof(Step2),
                typeof(Step3),
            });
        }

        // Step instantiation may be needed if you want to parameterize some steps.
        [TestMethod]
        [DataRow("Custom step")]
        [DataRow("Another step")]
        public void MoreExplicitParameterizedScenario(string customName)
        {
            new Workflow().Run(new List<IRunnable>{
                new Step1(),
                new Step3(customName)
            });
        }
    }

    [TestClass]
    public class StepsUnitTests
    {
        [TestMethod]
        public void Step1DoesWhatWeWant()
        {
            // Mock dependencies

            new Step1().Run();

            // Assert results
        }
    }

    #region Workflow Engine Example

    public interface IRunnable
    {
        void Run();
    }

    public class Workflow
    {
        public void Run(Scenario scenario)
        {
            Run(CreateSteps(scenario.GetStepTypes()));
        }

        public void Run(IEnumerable<Type> stepTypes)
        {
            Run(CreateSteps(stepTypes));
        }

        public void Run(List<IRunnable> steps)
        {
            steps.ForEach(step => step.Run());
        }

        private List<IRunnable> CreateSteps(IEnumerable<Type> stepTypes)
        {
            var steps = new List<IRunnable>();
            foreach (var stepType in stepTypes)
            {
                steps.Add(CreateStep(stepType));
            }

            return steps;
        }

        private IRunnable CreateStep(Type stepType)
            => (IRunnable) Activator.CreateInstance(stepType);
    }

    #endregion


    // Step structure can differ according to system requirements.
    // We may add data context and link steps into pipeline if needed.
    #region Steps

    public abstract class Step : IRunnable
    {
        private readonly string _stepName;

        protected Step(string name)
        {
            _stepName = name;
        }

        public void Run()
        {
            Console.WriteLine($"{_stepName} in action.");
            Invoke();
        }

        public abstract void Invoke();
    }

    public class Step1 : Step
    {
        public Step1() : base(nameof(Step1))
        {
        }

        public override void Invoke()
        {
            // do work
            Console.WriteLine($"Step1 invoked.");
        }
    }

    public class Step2 : Step
    {
        public Step2() : base(nameof(Step2))
        {
        }

        public override void Invoke()
        {
            // do work
            Console.WriteLine($"Step2 invoked.");
        }
    }

    public class Step3 : Step
    {
        public Step3(string customName) : base(customName)
        {

        }

        public Step3() : this(nameof(Step3))
        {
        }

        public override void Invoke()
        {
            // do work
            Console.WriteLine($"Step3 invoked.");
        }
    }

    public class Step4 : Step
    {
        public Step4() : base(nameof(Step4))
        {
        }

        public override void Invoke()
        {
            // do work
            Console.WriteLine($"Step4 invoked.");
        }
    }

    #endregion

    // Scenarios should be as declarative as possible.
    // Let's say the scenario is just specification of what steps (step Type)
    // and in what order should be executed (List as a non-unique ordered collection).
    #region Scenarios

    public abstract class Scenario
    {
        public abstract List<Type> GetStepTypes();
    }

    public class Scenario1 : Scenario
    {
        public override List<Type> GetStepTypes()
            => new List<Type>
            {
                typeof(Step1),
                typeof(Step2),
                typeof(Step3)
            };
    }

    public class Scenario2 : Scenario
    {
        public override List<Type> GetStepTypes()
            => new List<Type>
            {
                typeof(Step1),
                typeof(Step2),
                typeof(Step4)
            };
    }

    #endregion
}