Home > Software design >  xUnit testing method that utilises UI
xUnit testing method that utilises UI

Time:10-03

I am new to C#. I have been trying to write a xUnit Test for the method below ("ExecuteExportCsvCommand()"). However, it has some UI control as you can see below. Therefore, I don't know how to test the function. If anyone can explain, I would be really thankful.

 public void ExecuteExportCsvCommand()
    {
        ExecuteCancelPopOutWindowCommand();
        var previousState = View.IsEnabled;
        View.IsEnabled = false;
        try
        {
            var exportDialog = new ExportPrintDataDialog();
            var result = exportDialog.ShowDialog();

            if (result.GetValueOrDefault())
            {
                var projectData = _projectService.GetProject3dObject(Project.Id);

                IEnumerable<object> orderedList;
                orderedList = projectData.Select((x, i) => new ExportData {
                    Index = i,
                    Time = x.Time,
                    LayerNumber = x.LayerNumber,
                    X = x.X,
                    Y = x.Y,
                    Z = x.Z,
                    ArcVoltage = x.ArcVoltage,
                    Current = x.Current,
                    WireFeedSpeed = x.WireFeedSpeed,
                    WireFeedSpeedB = x.WireFeedSpeedB,
                    RatioWireFeedSpeed = (x.WireFeedSpeed == 0) ? 0 : x.WireFeedSpeedB / x.WireFeedSpeed,
                    BuildHeight = x.WallHeight,
                    LeadTemperature = x.LeadTemperature,
                    TrailTemperature = x.TrailTemperature,
                    OxygenLevel = x.OxygenLevel,
                    GasFlowA = x.GasFlow.First(),
                    GasFlowB = x.GasFlow.Last(),
                    TempChannels = x.TempChannels //preferably last as array of 8 values
                }).ToList();

                //Define Column headers
                var heightHeader = _options.CorriOptions.Value.UseWallHeight ? "Wall Height" : "Layer Height";
                var wfsHeader = Project.ProcessEnum == EnumProjectProcess.CW_MIG 
                    ? new []{ "Hot Wire Feed Speed", "Cold Wire Feed Speed" } 
                    : new[] { "Wire Feed Speed 1", "Wire Feed Speed 2" };
                var headers = new Dictionary<string, string>()
                {
                    { "Index" , "Ordinal Number" },
                    { "Time" , "Time" },
                    { "LayerNumber" , "Layer Number" },
                    { "X" , "Pos X" },
                    { "Y" , "Pos Y" },
                    { "Z" , "Pos Z" },
                    { "ArcVoltage" , "Arc Voltage" },
                    { "Current" , "Current" },
                    { "WireFeedSpeed" , wfsHeader.First() },
                    { "WireFeedSpeedB" , wfsHeader.Last()  },
                    {"RatioWireFeedSpeed", "R-Ratio"},
                    { "BuildHeight" , heightHeader },
                    { "LeadTemperature" , "Lead Temperature" },
                    { "TrailTemperature" , "Trail Temperature" },
                    { "OxygenLevel" , "Oxygen Level" },
                    { "GasFlowA" , "GasFlow Channel A" },
                    { "GasFlowB" , "GasFlow Channel B" },
                    { "TempChannels" , "End Effector Temperature (Channels 1 - 8)" }
                };

                var saveFileDialog = new SaveFileDialog
                {
                    Filter = "csv files (*.csv)|*.csv|All files (*.*)|*.*",
                    FilterIndex = 1,
                    RestoreDirectory = true,
                    FileName = string.Concat(DateTime.Now.ToString("yyyy-MM-ddTHHmm"), "_log"),
                };

                if (saveFileDialog.ShowDialog().GetValueOrDefault())
                {
                    using var writer = new StreamWriter(saveFileDialog.FileName);
                    using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

                    csv.Context.RegisterClassMap(new ExportDataMapper(headers));
                    csv.WriteRecords(orderedList);
                }
            }
        }
        finally
        {
            View.IsEnabled = previousState;
            ExportDataBtnChecked = false;
        }
    }

The test environment was set up already as you can see below. I just have to write the test for the method mentioned. If you need further details, please let me know. thanks.

public class ProjectPrintViewModelTests
{
    private readonly ProjectPrintViewModel _sut;
    private readonly IHost _host;

    private readonly Mock<IPermissionChecker> _permissionChecker = new Mock<IPermissionChecker>();
    private readonly Mock<ILogService> _logService = new Mock<ILogService>();
    private readonly Mock<IAuditService> _auditService = new Mock<IAuditService>();
    private readonly Mock<IProjectService> _projectService = new Mock<IProjectService>();

    public ProjectPrintViewModelTests()
    {
        var colorList = new List<string> {
            "DefaultMinColor",
            "DefaultLowColor",
            "DefaultNormColor",
            "DefaultHighColor",
            "DefaultMaxColor"
        };

        var app = new Application();
        colorList.ForEach(x => app.Resources.Add(x, Color.FromRgb(255, 255, 255)));

        _host = MockEnvironment.BuildHost();
        IoCContainer.SetServiceProvider(_host.Services);
        
        var options = _host.Services.GetRequiredService<OptionsContainer>();
        var valueService = _host.Services.GetRequiredService<IValuePointService>();
        var project = new ProjectViewModel(options);
        
        _sut = new ProjectPrintViewModel(
            options,
            project,
            _logService.Object,
            _permissionChecker.Object,
            _auditService.Object,
            _projectService.Object,
            valueService,
            false);
    }

    /// Write Tests Below Here

    [Fact]
    public void ExecuteCsvFileCommand_WhenValueAreValid()
    {
        //Assemble

        //Act
        _sut.ExecuteExportCsvCommand();

        //Assert

CodePudding user response:

This is one of the many disadvantages of having poor design and non-clean architecture.. Separating UI code from business logic helps you in testing your business logic well.

Keep in mind that unit tests should run/return as fast as possible. Apparently, showing a SaveFileDialog will stop the test until a user selects the path, this is wrong!

You should pull all UI-related code out to a separate component that delivers UI services, For example:

public interface IUiServices
{
    void CancelPopOutWindow();
    void SaveViewState();
    void RestoreViewState();
    bool ShowExportDialog();
    string ShowSaveFileDialog();
}

The most important part is, when testing, you'd mock the UIServices to not do any real UI work (Ex. opinging SaveFileDialog, it will just return a valid test path to save files in there).. So ExecuteExportCsvCommand can be look like this..


public void ExecuteExportCsvCommand()
{
    _uiServices.CancelPopOutWindow();
    _uiServices.SaveViewState();
    try
    {
        var b = _uiServices.ShowExportDialog();
        
        if (b)
        {
            var projectData = _projectService.GetProject3dObject(Project.Id);

            IEnumerable<object> orderedList;
            orderedList = projectData.Select((x, i) => new ExportData {
                Index = i,
                Time = x.Time,
                LayerNumber = x.LayerNumber,
                X = x.X,
                Y = x.Y,
                Z = x.Z,
                ArcVoltage = x.ArcVoltage,
                Current = x.Current,
                WireFeedSpeed = x.WireFeedSpeed,
                WireFeedSpeedB = x.WireFeedSpeedB,
                RatioWireFeedSpeed = (x.WireFeedSpeed == 0) ? 0 : x.WireFeedSpeedB / x.WireFeedSpeed,
                BuildHeight = x.WallHeight,
                LeadTemperature = x.LeadTemperature,
                TrailTemperature = x.TrailTemperature,
                OxygenLevel = x.OxygenLevel,
                GasFlowA = x.GasFlow.First(),
                GasFlowB = x.GasFlow.Last(),
                TempChannels = x.TempChannels //preferably last as array of 8 values
            }).ToList();

            //Define Column headers
            var heightHeader = _options.CorriOptions.Value.UseWallHeight ? "Wall Height" : "Layer Height";
            var wfsHeader = Project.ProcessEnum == EnumProjectProcess.CW_MIG 
                ? new []{ "Hot Wire Feed Speed", "Cold Wire Feed Speed" } 
                : new[] { "Wire Feed Speed 1", "Wire Feed Speed 2" };
            var headers = new Dictionary<string, string>()
            {
                { "Index" , "Ordinal Number" },
                { "Time" , "Time" },
                { "LayerNumber" , "Layer Number" },
                { "X" , "Pos X" },
                { "Y" , "Pos Y" },
                { "Z" , "Pos Z" },
                { "ArcVoltage" , "Arc Voltage" },
                { "Current" , "Current" },
                { "WireFeedSpeed" , wfsHeader.First() },
                { "WireFeedSpeedB" , wfsHeader.Last()  },
                {"RatioWireFeedSpeed", "R-Ratio"},
                { "BuildHeight" , heightHeader },
                { "LeadTemperature" , "Lead Temperature" },
                { "TrailTemperature" , "Trail Temperature" },
                { "OxygenLevel" , "Oxygen Level" },
                { "GasFlowA" , "GasFlow Channel A" },
                { "GasFlowB" , "GasFlow Channel B" },
                { "TempChannels" , "End Effector Temperature (Channels 1 - 8)" }
            };

            string path = _uiServices.ShowSaveFileDialog();
            
            if (!string.IsNullOrEmpty(path))
            {
                using var writer = new StreamWriter(path);
                using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
                csv.Context.RegisterClassMap(new ExportDataMapper(headers));
                csv.WriteRecords(orderedList);
            }
        }
    }
    finally
    {
        _uiServices.RestoreViewState();
        ExportDataBtnChecked = false;
    }
}
  • Related