Ich will folgende Klasse testen:
public class WeatherQuery
{
#region fields
...
#endregion
#region enums
public enum RequestType { Weather, Forecast}
#endregion
#region constructors
...
#endregion
#region methods
private string GetWeather(RequestType requestType, NameValueCollection parameters)
{
using (var client = new WebClient())
{
return client.DownloadString(BuildQueryURL("data/2.5/" + requestType.ToString().ToLower(),UpdateParameters(parameters)));
}
}
private async Task<string> GetWeatherAsync(RequestType requestType, NameValueCollection parameters)
{
...
}
public string GetWeather(RequestType requestType, string city, string countryCode)
{
return GetWeather(requestType, new NameValueCollection() { { "q", CombineQueryCountryCode(city,countryCode) } });
}
public string GetWeather(RequestType requestType, double latitude, double longitude)
{
return GetWeather(requestType, new NameValueCollection() { { "lat", latitude.ToString(CultureInfo.InvariantCulture) }, { "lon", longitude.ToString(CultureInfo.InvariantCulture) } });
}
public async Task<string> GetWeatherAsync(RequestType requestType, string city, string countryCode)
{
...
}
public async Task<string> GetWeatherAsync(RequestType requestType, double latitude, double longitude)
{
...
}
public byte[] GetIcon(string iconIdentifier)
{
using (var client = new WebClient())
{
return client.DownloadData(BuildQueryURL("img/w/",null));
}
}
public async Task<byte[]> GetIconAsync(string iconIdentifier)
{
using (var client = new WebClient())
{
return await client.DownloadDataTaskAsync(BuildQueryURL("img/w/" + iconIdentifier,null));
}
}
private Uri BuildQueryURL(string relativePath, NameValueCollection parameters)
{
if (parameters == null) parameters = new NameValueCollection();
//build uri//
var builder = new UriBuilder(_scheme, _host);
builder.Query = string.Join("&", parameters.AllKeys.Select((name) => string.Format("{0}={1}", HttpUtility.UrlEncode(name), HttpUtility.UrlEncode(parameters[name]))));
return builder.Uri;
}
private NameValueCollection UpdateParameters(NameValueCollection parameters)
{
...
}
private string CombineQueryCountryCode(string query, string countryCode)
{
return !string.IsNullOrEmpty(countryCode) ? query + "," + countryCode : query;
}
#endregion
}
Mir sind schon zwei Tests eingefallen, die ich gerade aber nicht umsetzten kann:
-Obwohl es Sinn macht Methode BuildQueryURL zu testen, macht es keinen Sinn sie public zu machen.
-Ich müsste in jeder Methode ein WebClient übergeben, um auch ein Mockobjekt verwenden zu können. Gibt es eine elegantere Lösung?
Welche Tests wären sonst noch sinnvoll?
Schau Dir den Repository Pattern an. Nichts anderes baust Du eigentlich gerade, ohne dass Du weisst, dass es das gibt.
Einiges fehlt Dir aber (ich verwende jetzt einfach mal die Begrifflichkeit Repository
):
Prinzipiell ist in der Implementierung "WeatherRepository
" alles public und alles testbar.
Alles, was Du nach aussen gibst, gibst Du dann über ein Interface IWeatherRepository
preis - hast hier also zB keine Methode, die bei Dir aktuell private
ist.
Zudem vermeidet man - gut, dass Du es jetzt merkst - jegliche Verwendung von direkten Ressourcen wie den WebClient direkt im Repository.
Du würdest hier eher eine Klasse machen, die HttpService
heisst und für Dich den WebClient
implementiert/wrappt.
Zudem hast Du aber einen IHttpService
Interface.
Das Repository bekommt über den Konstruktor das IHttpService injeziert (Dependency Injection) und verwendet dann quasi nur die Instanz, die über den Konstruktor kommt.
Was hinter dem Interface steckt muss dem Repository egal sein.n
Beim produktiven Code steckt dahinter dann das HttpService
Konstrukt, das den WebClient
verwendet.
Beim Unit-Testen mockst Du aber das IHttpService
Interface, sodass Du beim Testen KEIN echten Webrequests sendest.
Schau Dir hier das NuGet-Paket "Moq
" an, mit dem man solche Mocks realisiert.
var httpServiceMock = new Moq<IHttpService>();
httpServiceMock.Setup ( s => s.Get(..)).Returns(...);
Beim Integrationstest dann verwendest Du keinen Mock, sondern das echte WebClient-Konstrukt.
Du hast also Unit-Tests und Integrationstests.
public interface IHttpService {}
public class HttpService : IHttpService {}
public class WeatherRepository : IWeatherRepository
{
private IHttpService _httpService;
public WeatherRepository (IHttpService httpService)
{
}
public .. GetAsync ( ...)
{
var response = _httpService.GetAsync(..);
}
}
Überleg Dir, ob es sinn macht, wenn eine Methode GetWeather heisst, aber einen string zurück gibt.
Ich sage nein. Ich würde hier ein Weather
-Objekt erwarten.
Übrigens steht Mocking auch in dem Link, den ich Dir erst vor 2h in Deinem anderen Thread bereits gegeben habe:
[Artikel] Unit-Tests: Einführung in das Unit-Testing mit VisualStudio
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Danke! Ich verstehe, dass ich IHTTPService für Mocking brauche, aber wieso IWeatherRepository?
Damit du die Teile deiner SW testen kannst, die dieses Repository benutzen.
Lies dich einfach mal in "Inversion Of Control" ein.