Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
lawrence_shaun_introducing_net_maui_build_and_deploy_crosspl.pdf
Скачиваний:
46
Добавлен:
26.06.2023
Размер:
5.15 Mб
Скачать

Chapter 12 Testing

};

viewModel.BoardName = "Work";

Assert.True(invoked);

}

This provides you with the confidence to know that if the BoardName is not showing in your user interface, it will probably not be an issue inside the view model.

Testing Asynchronous Operations

Many modern applications involve some level of asynchronous operation and a perfect example is your use of the Open Weather API in order to load the current location’s weather. The WeatherWidgetViewModel relies on an implementation of the IWeatherForecastService interface you created in Chapter 10. Unit tests against specific implementations like this can

be considered flaky. A flaky test is one that provides inconsistent results. Web service access can exhibit this type of behavior when unit testing given access limits on the API or other potential issues that could impact a reliable test run.

In order to remove test flakiness, you can create a mock implementation that will provide a set of consistent behavior.

Creating Your ILocationService Mock

Create a new folder in your WidgetBoard.Tests project and call it Mocks. I covered this before but organizing your code in such a way really can make it much easier to maintain. With this new folder, you can create a new class file inside and call it MockLocationService.cs. Modify the contents to the following:

374

Chapter 12 Testing

using WidgetBoard.Services;

namespace WidgetBoard.Tests.Mocks;

internal class MockLocationService : ILocationService

{

private readonly Location? location; private readonly TimeSpan delay;

private MockLocationService(Location? mockLocation, TimeSpan delay)

{

location = mockLocation; this.delay = delay;

}

internal static ILocationService ThatReturns(Location? location, TimeSpan after) =>

new MockLocationService(location, after);

internal static ILocationService ThatReturnsNoLocation(TimeSpan after) =>

new MockLocationService(null, after);

public async Task<Location?> GetLocationAsync()

{

await Task.Delay(this.delay);

return this.location;

}

}

The implementation you provided for the GetLocationAsync method forces a delay based on the supplied TimeSpan parameter in the constructor to mimic a network delay and then return the location supplied in the constructor.

375

Chapter 12 Testing

One key detail I really like to use when building mocks is to make the usage of them in my tests as easy to read as possible. You can see that the MockLocationService cannot be instantiated because it has a private constructor. This means that to use it you must use the ThatReturns or ThatReturnsNoLocation methods. Look at this and see how much more readable it is:

MockLocationService.ThatReturns(new Location(0.0, 0.0), after: TimeSpan.FromSeconds(2));

The above is much more readable than the following because it includes the intent:

new MockLocationService(new Location(0.0, 0.0), TimeSpan. FromSeconds(2));

Creating Your WeatherForecastService Mock

You can add a second file into the Mocks folder and call this class file MockWeatherForecastService.cs. Modify the contents to the following:

using WidgetBoard.Communications;

namespace WidgetBoard.Tests.Mocks;

internal class MockWeatherForecastService : IWeatherForecastService

{

private readonly Forecast? forecast; private readonly TimeSpan delay;

private MockWeatherForecastService(Forecast? forecast, TimeSpan delay)

{

this.forecast = forecast; this.delay = delay;

376

Chapter 12 Testing

}

internal static IWeatherForecastService ThatReturns(Forecast? forecast, TimeSpan after) =>

new MockWeatherForecastService(forecast, after);

internal static IWeatherForecastService ThatReturnsNoForecast(TimeSpan after) =>

new MockWeatherForecastService(null, after);

public async Task<Forecast?> GetForecast(double latitude, double longitude)

{

await Task.Delay(this.delay);

return forecast;

}

}

The implementation you provided for the GetForecast method forces a delay based on the supplied TimeSpan parameter in the constructor

to mimic a network delay and then return the forecast supplied in the constructor.

Creating Your Asynchronous Tests

With your mocks in place, you can write tests that will verify the behavior of your application when calling asynchronous and potentially long running operations. You need to add a new class file to your

ViewModels folder in the WidgetBoard.Tests project and call is WeatherWidgetViewModelTests.cs and then modify the contents to the following:

using WidgetBoard.Tests.Mocks; using WidgetBoard.ViewModels;

377

Chapter 12 Testing

namespace WidgetBoard.Tests.ViewModels;

public class WeatherWidgetViewModelTests

{

}

Now you can proceed to adding three tests to cover a variety of different scenarios.

[Fact]

public async Task NullLocationResultsInPermissionErrorState()

{

var viewModel = new WeatherWidgetViewModel( MockWeatherForecastService.ThatReturnsNoForecast(after: TimeSpan.FromSeconds(5)), MockLocationService.ThatReturnsNoLocation(after: TimeSpan.FromSeconds(2)));

await viewModel.InitializeAsync();

Assert.Equal(State.PermissionError, viewModel.State); Assert.Null(viewModel.Weather);

}

This first test, as the name implies, verifies that if a null location is returned from the ILocationService implementation, the view model

State will be set to PermissionError and no Weather will be set.

[Fact]

public async Task NullForecastResultsInErrorState()

{

var viewModel = new WeatherWidgetViewModel( MockWeatherForecastService.ThatReturnsNoForecast(after: TimeSpan.FromSeconds(5)),

378

Chapter 12 Testing

MockLocationService.ThatReturns(new Location(0.0, 0.0), after: TimeSpan.FromSeconds(2)));

await viewModel.InitializeAsync();

Assert.Equal(State.Error, viewModel.State); Assert.Null(viewModel.Weather);

}

This second test, as the name implies, verifies that if a null forecast is returned from the IWeatherForecastService implementation, the view model State will be set to Error and no Weather will be set.

[Fact]

public async Task ValidForecastResultsInSuccessfulLoad()

{

var weatherForecastService = MockWeatherForecastService.ThatReturns(

new Communications.Forecast

{

Current = new Communications.Current

{

Temperature = 18.0,

Weather = new Communications.Weather[]

{

new Communications.Weather

{

Icon = "abc.png",

Main = "Sunshine"

}

}

}

},

379