Laden...

Klasse zur API-Interaktion testen

Erstellt von R3turnz vor 7 Jahren Letzter Beitrag vor 7 Jahren 1.408 Views
R
R3turnz Themenstarter:in
125 Beiträge seit 2016
vor 7 Jahren
Klasse zur API-Interaktion testen

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?

16.834 Beiträge seit 2008
vor 7 Jahren

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

R
R3turnz Themenstarter:in
125 Beiträge seit 2016
vor 7 Jahren

Danke! Ich verstehe, dass ich IHTTPService für Mocking brauche, aber wieso IWeatherRepository?

F
10.010 Beiträge seit 2004
vor 7 Jahren

Damit du die Teile deiner SW testen kannst, die dieses Repository benutzen.

Lies dich einfach mal in "Inversion Of Control" ein.