Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Create Video Recording Engine - Usage in Automated Tests

5.00/5 (4 votes)
21 Aug 2016Ms-PL5 min read 10.4K  
Learn how to create a recording engine for your automated tests. Configure it through attributes and even change its detailed implementation from a single location.The post Create Video Recording Engine - Usage in Automated Tests appeared first on Automate The Planet.

Introduction

The next article from the ‘Design & Architecture Series’ is going to be dedicated to an interesting topic- video recording of a desktop. More specifically, I am going to show you how to create a video recording engine whose implementation can be changed with a single line of code. Moreover, you will be able to configure the engine completely via attributes. The tests’ integration is implemented through the Observer Design Pattern (previously discussed in the series).

Video Recording Engine

IVideoRecordingEngine Interface

The first thing that we need to do is to create a new interface for our video recorder. It should be as simple as possible. Because of that, it contains only two methods. StartCapture begins the video recording and as you can easily guess, the SaveVideo saves the recorded video to a particular location. Through the Status property, you can check the current state of the player. Our interface derives from the IDisposable interface because the recording engines need to free some resources before and after the saving of the recorded video.

C#
public interface IVideoRecorder : IDisposable
{
    VideoRecordingStatus Status { get; }

    Model.VideoRecordingResult StartCapture();

    VideoRecordingResult SaveVideo(string saveLocation, string testName);
}

VideoRecordingStatus

C#
public enum VideoRecordingStatus
{
    NotStarted = 0,
    Stopped = 1,
    Paused = 2,
    Running = 3
}

The player can be only in four modes - not started, stopped, paused and running.

VideoRecordingResult

C#
public class VideoRecordingResult
{
    public bool IsSuccessfullySaved { get; set; }

    public Exception SavedException { get; set; }

    public VideoRecordingResult(bool isSuccessfullySaved = true)
    {
        this.IsSuccessfullySaved = isSuccessfullySaved;
    }
}

Both methods return instances of the VideoRecordingResult. Most of the time, you don’t need information from these methods. However, I think the interface is more intuitive if the methods indicate whether they succeeded or not. Also, you don’t want your tests to fail if the video recording engine throws an exception like not enough space on the machine or insufficient permissions. The error is wrapped in the result object.

Microsoft Expression Encoder 4 Setup

The first implementation of the video recording engine uses the Microsoft Expression Encoder 4 SDK. To be able to use on your machine, you first need to download and install the SDK. Otherwise, you won’t have the necessary DLLs. Currently, the encoder isn’t available as a NuGet package.

MsExpressionEncoderVideoRecorder

ScreenCaptureJob is the main class of the Microsoft Expression Encoder. In the Initialize method, we configure its properties such as quality, frame rate, the size of the recording screen, should the cursor be recorded and so on. Through decreasing/increasing the Quality and FrameRate constants, you can control the size of the output video file. There is a tricky part in the configuration of the primary screen's height and width. When their values are divided by 16, the remainder should be equal to zero. Otherwise, the screenCaptureJob will throw an exception.

The usage of the capture job is a trivial task. There are two important methods - Start and Stop. Once the capturing finishes, you can get the path to the file via the OutputScreenCaptureFileName property. It points to a temp file path that we specify in the Initialize method. However, the SaveVideo method accepts a different location and the file is moved there. The final file is in ".wmv" format. Also, it contains the name of the recorded test plus a time stamp in its name.

C#
public class MsExpressionEncoderVideoRecorder : IVideoRecorder
{
    private const string VideoExtension = ".wmv";
    private const string NewFileDateTimeFormat = "yyyyMMddHHmmssfff";
    private const int FrameRate = 5;
    private const int Quality = 20;
    private readonly int height = 
        Screen.PrimaryScreen.Bounds.Height - (Screen.PrimaryScreen.Bounds.Height % 16);
    private readonly int width = 
        Screen.PrimaryScreen.Bounds.Width - (Screen.PrimaryScreen.Bounds.Width % 16);
    private ScreenCaptureJob screenCaptureJob;
    private bool isDisposed;

    public VideoRecordingStatus Status
    {
        get
        {
            return (VideoRecordingStatus)this.screenCaptureJob.Status;
        }
    }

    public VideoRecordingResult StartCapture()
    {
        VideoRecordingResult result = new VideoRecordingResult();
        try
        {
            this.Initialize();
            this.screenCaptureJob.Start();
        }
        catch (Exception ex)
        {
            string argumentExceptionMessage = 
                string.Format("Video capturing failed with the following exception:{0}. 
                               Resolution: width - {1}, height - {2}. ",
                ex.Message,
                this.height,
                this.width);
            result.SavedException = new ArgumentException(argumentExceptionMessage);
            result.IsSuccessfullySaved = false; 
        }

        return result;
    }

    public void StopCapture()
    {
        this.screenCaptureJob.Stop();
    }

    public VideoRecordingResult SaveVideo(string saveLocation, string testName)
    {
        VideoRecordingResult result = new VideoRecordingResult();

        try
        {
            this.StopCapture();
        }
        catch (Exception e)
        {
            result.SavedException = e;
            result.IsSuccessfullySaved = false;
        }
         
        if (Directory.Exists(saveLocation))
        {
            string moveToPath = this.GenerateFinalFilePath(saveLocation, testName);
            File.Move(this.screenCaptureJob.OutputScreenCaptureFileName, moveToPath);
        }
        else
        {
            result.SavedException = 
                new ArgumentException("The specified save location does not exists."); 
            result.IsSuccessfullySaved = false; 
        }

        return result;
    }

    public void Dispose()
    {
        if (!this.isDisposed)
        {
            if (this.Status == VideoRecordingStatus.Running)
            {
                this.StopCapture();
            }
            this.DeleteTempVideo();
            this.isDisposed = true;
        }
    }

    private void Initialize()
    {
        this.screenCaptureJob = new ScreenCaptureJob();
        this.screenCaptureJob.CaptureRectangle = new Rectangle(0, 0, this.width, this.height);
        this.screenCaptureJob.ScreenCaptureVideoProfile.Force16Pixels = true;
        this.screenCaptureJob.ShowFlashingBoundary = true;
        this.screenCaptureJob.ScreenCaptureVideoProfile.FrameRate = FrameRate;
        this.screenCaptureJob.CaptureMouseCursor = true;
        this.screenCaptureJob.ScreenCaptureVideoProfile.Quality = Quality;
        this.screenCaptureJob.ScreenCaptureVideoProfile.Size = new Size(this.width, this.height);
        this.screenCaptureJob.ScreenCaptureVideoProfile.AutoFit = true;
        this.screenCaptureJob.OutputScreenCaptureFileName = this.GetTempFilePathWithExtension();
        this.isDisposed = false;
    }

    private string GenerateFinalFilePath(string saveLocation, string testName)
    {
        string newFileName = 
            string.Concat(
            testName, 
            "-",
            DateTime.Now.ToString(NewFileDateTimeFormat), 
            VideoExtension);
        string moveToPath = Path.Combine(saveLocation, newFileName);
        return moveToPath;
    }

    private string GetTempFilePathWithExtension()
    {
        var path = Path.GetTempPath();
        var fileName = string.Concat(Guid.NewGuid().ToString(), VideoExtension);
        return Path.Combine(path, fileName);
    }

    private void DeleteTempVideo()
    {
        if (File.Exists(this.screenCaptureJob.OutputScreenCaptureFileName))
        {
            File.Delete(this.screenCaptureJob.OutputScreenCaptureFileName);
        }
    }
}

Everything happens, because of that, our video recorder implements the IDisposable interface. If the client of the recorder for some reason doesn't stop the recording or save the video, the Dispose method must be called. It stops the capturing if it is still running and deletes the temp video file.

Video Recording Engine- Integration in Tests

VideoRecordingAttribute

We are going to configure the recording engine in the same manner as the execution engine discussed in the previous article from the series (Dynamically Configure Execution Engine). The attribute can be set again on class or method level. This time, it contains a single property of type VideoRecordingMode which specifies when the video recording should be performed.

C#
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
Inherited = true,
AllowMultiple = false)]
public sealed class VideoRecordingAttribute : Attribute
{
    public VideoRecordingAttribute(VideoRecordingMode videoRecordingMode)
    {
        this.VideoRecording = videoRecordingMode;
    }

    public VideoRecordingMode VideoRecording { get; set; }
}

VideoRecordingMode

You can always record the tests' execution or only for failed/passed tests. Also, you can turn off the recording completely.

C#
public enum VideoRecordingMode
{
    Always,
    DoNotRecord,
    Ignore,
    OnlyPass,
    OnlyFail
}

VideoBehaviorObserver

I am not going to explain again in much details how the Observer solution works, if you haven't read my articles about it, I suggest to do so - Advanced Observer Design Pattern via Events and Delegates and Dynamically Configure Execution Engin??e. In the PreTestInit method, we get the specified VideoRecordingMode. The global app.config configuration ShouldTakesVideosOnExecution if set overrides all attributes. As in the previous examples, the attributes on a method level override those on class level. We get the values of these attributes through reflection. If the mode is not equal to DoNotRecord, we start the video recording.

C#
public class VideoBehaviorObserver : BaseTestBehaviorObserver
{
    private readonly IVideoRecorder videoRecorder;
    private VideoRecordingMode recordingMode;
        
    public VideoBehaviorObserver(IVideoRecorder videoRecorder)
    {
        this.videoRecorder = videoRecorder;
    }

    protected override void PostTestInit(object sender, TestExecutionEventArgs e)
    {
        this.recordingMode = this.ConfigureTestVideoRecordingMode(e.MemberInfo);

        if (this.recordingMode != VideoRecordingMode.DoNotRecord)
        {
            this.videoRecorder.StartCapture();
        }
    }

    protected override void PostTestCleanup(object sender, TestExecutionEventArgs e)
    {
        try
        {
            string videosFolderPath = ConfigurationManager.AppSettings["videosFolderPath"];
            string testName = e.TestName;
            bool hasTestPassed = e.TestOutcome.Equals(TestOutcome.Passed);
            this.SaveVideoDependingOnTestoutcome(videosFolderPath, testName, hasTestPassed);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            throw;
        }
        finally
        {
            this.videoRecorder.Dispose();
        }
    }

    private void SaveVideoDependingOnTestoutcome(
    string videoFolderPath, 
    string testName, 
    bool haveTestPassed)
    {
        if (this.recordingMode != VideoRecordingMode.DoNotRecord &&
            this.videoRecorder.Status == VideoRecordingStatus.Running)
        {
            bool shouldRecordAlways = 
            this.recordingMode == VideoRecordingMode.Always;
            bool shouldRecordAllPassedTests = 
            haveTestPassed && this.recordingMode.Equals(VideoRecordingMode.OnlyPass);
            bool shouldRecordAllFailedTests = 
            !haveTestPassed && this.recordingMode.Equals(VideoRecordingMode.OnlyFail);
            if (shouldRecordAlways || shouldRecordAllPassedTests || shouldRecordAllFailedTests)
            {
                this.videoRecorder.SaveVideo(videoFolderPath, testName);
            }
        }
    }

    private VideoRecordingMode ConfigureTestVideoRecordingMode(MemberInfo memberInfo)
    {
        VideoRecordingMode methodRecordingMode = 
        this.GetVideoRecordingModeByMethodInfo(memberInfo);
        VideoRecordingMode classRecordingMode = 
        this.GetVideoRecordingModeType(memberInfo.DeclaringType);
        VideoRecordingMode videoRecordingMode = VideoRecordingMode.DoNotRecord;
        bool shouldTakeVideos = 
        bool.Parse(ConfigurationManager.AppSettings["shouldTakeVideosOnExecution"]);
            
        if (methodRecordingMode != VideoRecordingMode.Ignore && shouldTakeVideos)
        {
            videoRecordingMode = methodRecordingMode;
        }
        else if (classRecordingMode != VideoRecordingMode.Ignore && shouldTakeVideos)
        {
            videoRecordingMode = classRecordingMode;
        }
        return videoRecordingMode;
    }

    private VideoRecordingMode GetVideoRecordingModeByMethodInfo(MemberInfo memberInfo)
    {
        if (memberInfo == null)
        {
            throw new ArgumentNullException("The test method's info cannot be null.");
        }

        var recordingModeMethodAttribute = 
        memberInfo.GetCustomAttribute<VideoRecordingAttribute>(true);
        if (recordingModeMethodAttribute != null)
        {
            return recordingModeMethodAttribute.VideoRecording;
        }
        return VideoRecordingMode.Ignore;
    }

    private VideoRecordingMode GetVideoRecordingModeType(Type currentType)
    {
        if (currentType == null)
        {
            throw new ArgumentNullException("The test method's type cannot be null.");
        }

        var recordingModeClassAttribute = 
        currentType.GetCustomAttribute<VideoRecordingAttribute>(true);
        if (recordingModeClassAttribute != null)
        {
            return recordingModeClassAttribute.VideoRecording;
        }
        return VideoRecordingMode.Ignore;
    }
}

Most of the work is done in the PostTestCleanup method of the observer. The final video location is specified again in the app.config with the key- videosFolderPath. In order for the code to be more testable, we can move this configuration to a dedicated configuration class and use it here as an interface. Based on the test's outcome and the specified video recording mode, we decide whether to save the file or not. All of the code here is surrounded by a try-catch-finally. In the finally block, we call the Dispose method of the video recording engine.

Tests Examples

BingTests

Additionally to the previously created ExecutionEngineAttribute, we add the new VideoRecordingAttribute. It is configured to save the videos only for the failed tests. Because of that, we fail the tests through the Assert.Fail() method. The videos are saved in the folder specified in the app.config.

C#
[TestClass,
ExecutionEngineAttribute(ExecutionEngineType.TestStudio, Browsers.Firefox),
VideoRecordingAttribute(VideoRecordingMode.OnlyFail)]
public class BingTests : BaseTest
{
    [TestMethod]
    public void SearchForAutomateThePlanet()
    {
        var bingMainPage = this.Container.Resolve<BingMainPage>();
        bingMainPage.Navigate();
        bingMainPage.Search("Automate The Planet");
        bingMainPage.AssertResultsCountIsAsExpected(264);
        Assert.Fail();
    }

    [TestMethod]
    public void SearchForAutomateThePlanet1()
    {
        var bingMainPage = this.Container.Resolve<BingMainPage>();
        bingMainPage.Navigate();
        bingMainPage.Search("Automate The Planet");
        bingMainPage.AssertResultsCountIsAsExpected(264);
        Assert.Fail();
    }
}

Design & Architecture

The post Create Hybrid Test Framework – Testing Framework Driver Implementation appeared first on Automate The Planet.

All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)