Laden...

Testen einer API Wrapper Library

Erstellt von Duesmannr vor einem Jahr Letzter Beitrag vor einem Jahr 685 Views
D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor einem Jahr
Testen einer API Wrapper Library

Nabend,
falls dies nicht der richtige Bereich sein sollte, gerne verschieben.

Ich habe eine Library für die API von TMDB geschrieben in .NET 6.
API Doku ist hier zu finden.
Dabei nutze ich Refit um die Requests zu senden.

Für die Library würde ich gerne Tests schreiben um die Funktionalität der Library zu gewährleisten.
Ich nutze das Test Framework von Microsoft "MSTest".

Ich möchte gerne Wissen ob mein Vorgehen dabei das richtige ist.

Es gibt mehrere Endpunkte wo man Informationen holen kann, zB. von Filmen/Serien/Personen.
Da würde ich einen Test schreiben wo der Service authentifiziert ist und eine richtige Film Id übergibt. Sowie der Service nicht authentifiziert ist und eine Exception wirft und ich eine Id übergebe die nicht existiert.

Es gibt auch einen Endpunkt wo man Filme und Serien als Favorit markieren kann. Meine Library Implementation sieht wie folgt aus:


private async Task<Response> MarkAsFavoriteAsync(int accountId, int mediaId, AccountMediaType mediaType, bool favorite)
{
    this.CheckAuthentication();
    this.CheckSession();

    MarkAsFavoriteRequest markAsFavoriteRequest = new(mediaType.GetString(), mediaId, favorite);

    try
    {
        return this._authenticationType == AuthenticationType.ApiKey
                ? await this.AccountApi.MarkAsFavoriteAsync(accountId, this._sessionId!, markAsFavoriteRequest, this._token).ConfigureAwait(false)
                : await this.AccountApi.MarkAsFavoriteAsync(accountId, this._sessionId!, markAsFavoriteRequest).ConfigureAwait(false);
    }
    catch (ApiException ex)
    {
        if(string.IsNullOrWhiteSpace(ex.Content))
        {
            return new();
        }

        return JsonSerializer.Deserialize<Response>(ex.Content) ?? new();
    }
}

AccountApi ist das Interface um den API Call mittels Refit zu senden.
Die Methode wird von dieser hier aufgerufen:


public async Task<Response> MarkMovieAsFavoriteAsync(int accountId, int movieId, bool favorite)
{
    return await this.MarkAsFavoriteAsync(accountId, movieId, AccountMediaType.Movie, favorite).ConfigureAwait(false);
}

Die 2. Methode ist in einem Interface angegeben und das Interface benutze ich in der Test Klasse.
Jetzt weiß ich nicht, wie ich diese Methode am besten teste.
Wenn ich einen Film als Favorit markiere, bekomme ich die Rückmeldung, dass ich den Film erfolgreich als Favorit markiert habe.
Sende ich den Request jetzt nochmal, bekomme ich die Rückmeldung, dass der Eintrag geupdated worden ist.
Müsste ich die Markierung nach dem Ende des Tests nicht zurücksetzen? Wie handhabt man das bei einer öffentlichen API?
Und wie teste ich das in diesem konkreten Fall am besten?

Das sind zwei Tests die ich für die Methode geschrieben habe:


[TestMethod]
[TestProperty(nameof(AccountService), nameof(AccountService_MarkMovieAsFavorite))]
[TestCategory(IntegrationTestCategoryName)]
public async Task AccountService_MarkMovieAsFavorite()
{
	//Arrange
	this._accountService.UseExistingSessionId(this.AccountSession!);
	int movieId = 60735;
	bool favorite = true;
	bool success = true;

	//Act
	Response response = await this._accountService.MarkMovieAsFavoriteAsync(this.AccountId, movieId, favorite);

	//Assert
	Assert.IsNotNull(response);
	Assert.AreEqual(success, response.Success);
}

[TestMethod]
[TestProperty(nameof(AccountService), nameof(AccountService_MarkMovieAsFavorite_InvalidValues))]
[TestCategory(IntegrationTestCategoryName)]
public async Task AccountService_MarkMovieAsFavorite_InvalidValues()
{
	//Arrange
	this._accountService.UseExistingSessionId(this.AccountSession!);
	int movieId = 0;
	bool favorite = true;
	bool success = false;

	//Act
	Response response = await this._accountService.MarkMovieAsFavoriteAsync(this.AccountId, movieId, favorite);

	//Assert
	Assert.IsNotNull(response);
	Assert.AreEqual(success, response.Success);
}

Schönen Abend noch!
René

2.078 Beiträge seit 2012
vor einem Jahr

Du testest die externe API mit, das willst Du nicht, denn da ändern sich natürlich die Daten und es ist langsam.
Bei UnitTests willst Du das testen, worüber Du die Kontrolle hast und deine Kontrolle endet bei Refit.

Ich würde also in den UnitTests die Refit-Interfaces mocken, immer so, wie es deine Tests brauchen.
Dann kannst Du performant ohne zuverlässig reproduzierbar deine Tests aufbauen.

By the way:
Guck dir mal xUnit an, ich mag's lieber.

16.806 Beiträge seit 2008
vor einem Jahr

Du hast da ja mehrere Faktoren.

Zum einen hast Du Refit als API Beschreibung, zum anderen willst Du wohl, wenn ich Dich richtig versteh, auch einen interaktiven Client zur Verfügung stellen, der auch logische Aufgaben erledigt.
Refit selbst ist ja deklarativ, kann man aber mit MockHttp testen.
Deinen Client, also die Logik, kannst Du ebenfalls "normal" über Unit Tests abdecken, zusammen mit Hilfsmitteln wie Moq, XUnit, FluentAssertions, AutoFixture (je nach Gefallen, das sind meine Standard-Kandidaten fürs Testen).

Feedback zum Code: das wird so definitiv aufwendig zu testen 😉
Modularier Deinen Code und verwende die Hilfsmittel von Refit, dann musst Du auch nicht so Zeug behandeln wie


    this.CheckAuthentication();
    this.CheckSession();

weil dafür gibts die HttpHandler / DelegatingHandler. Genauso für das ganze Thema Exception Handling etc.

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor einem Jahr

Danke für euren Input.

Dann werde ich wohl auf XUnit umsteigen und werde mir die HttpHandler genauer ansehen.
Ans Mocken hatte ich gar nicht gedacht, aber muss man ja, wenn man die externe API nicht testen will.

@Abt ich habe noch keine Idee wie ich das Exception Handling mit Refit machen soll. Die Snippets auf der Github Seite helfen mir da bedingt weiter.
Was ich damit meine ist, was ist der Way2Go für Exceptions in einer Library? Die ApiException sollte ich ja schon behandeln oder nicht?
Werfe ich dann eine neue custom Exception?

16.806 Beiträge seit 2008
vor einem Jahr

So wie man es in .NET immer macht: generelle Exceptions abfangen und eigene Exceptions verwenden:
https://docs.microsoft.com/en-us/dotnet/standard/exceptions/
In den meisten Fällen folgt man den Gedanken, dass man selbst die Lib (hier Refit) wegabstrahiert, um nach Außen keine Abhängigkeit zu schaffen, die Du hättest, wenn Du einfach die ApiException roh weiter geben würdest.

Heisst in Deinem Fall: ApiException behandeln und in spezifische Exceptions übersetzen.
Dafür hat Refit einen Mechanismus, steht auch in den Docs.

2.921 Beiträge seit 2005
vor einem Jahr

Zusatztipp an den TE, wenn Du dann tatsächlich XUnit benutzen möchtest, dann:

Kannst Du auch gleichzeitig das hier mitbenutzen und Dir eine Menge Arbeit sparen:


    [AttributeUsage(AttributeTargets.Method)]
    public sealed class AutoMoqDataXUnitAttribute : AutoDataAttribute
    {
        public AutoMoqDataXUnitAttribute()
            : base(() => new Fixture().Customize(new AutoMoqCustomization()))
        {
        }
    }


P.S.: Funktioniert genauso mit NUnit, wenn man die entsprechenden NuGet Packages benutzt.


        [Theory, AutoMoqDataXUnit]
        public void TestHelpAction(HelpAction action)
        {
              [...] //here goes your Code
        }


Damit werden dann Deine Objekte automatisch initialisert und Du kannst sie dennoch kontrollieren, falls nötig und hast dann auch gleich AutoMoq und AutoFixture drin.
Denke das sollte noch zusätzlich helfen.

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

16.806 Beiträge seit 2008
vor einem Jahr

Das funktioniert aber mit fast allen Test Suites (auch NUnit etc), weshalb man die Klasse nicht XUnit spezifisch nennen muss.
Einzig MSTest hat derzeit keinen Constructor Support.

XUnit


public class Tests
{
    [Theory, AutoMoqData]
    public void Test(Mock<IMyStuff> stuffMoq)

NUnit


[TestFixture]
public class Tests
{
    [Test, AutoMoqData]
    public void Test(Mock<IMyStuff> stuffMoq)

So testen wir hier im Forum auch fast alle Dependencies.