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.
public interface IVideoRecorder : IDisposable
{
VideoRecordingStatus Status { get; }
Model.VideoRecordingResult StartCapture();
VideoRecordingResult SaveVideo(string saveLocation, string testName);
}
VideoRecordingStatus
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
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.
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.
[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.
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 PreTes
tInit
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.
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.
[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
CodeProject
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