Unit Testing Umami.Net M SK1 Umami-tietojen testaaminen Moq:n käyttämättä
Esitys
Edellinen osa tästä sarjasta, jossa testasin Umami.Nettiseurantamenetelmät
Ongelma
Edellisen osan aikana käytin Moq:ta antaakseni Mock<HttpMessageHandler> ja palauttaa käsittelijä, jota käytetään UmamiClient, tämä on yleinen malli testattaessa koodia, jossa käytetään HttpClient. Tässä artikkelissa kerron teille, miten testata uusi UmamiDataService käyttämättä Moq.
public static HttpMessageHandler Create()
{
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.ToString().Contains("api/send")),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync((HttpRequestMessage request, CancellationToken cancellationToken) =>
{
// Read the request content
var requestBody = request.Content != null
? request.Content.ReadAsStringAsync(cancellationToken).Result
: null;
// Create a response that echoes the request body
var responseContent = requestBody != null
? requestBody
: "No request body";
// Return the response
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(responseContent, Encoding.UTF8, "application/json")
};
});
return mockHandler.Object;
}
Miksi Moq?
Moq on tehokas mocking-libraari, jonka avulla voidaan luoda malliesineitä liittymiin ja luokkiin.. Sitä käytetään laajasti yksiköiden testauksessa eristääkseen testattavan kodin sen riippuvuussuhteista.
Edellä mainitsemani esimerkki antaa paljon joustavuutta testattaessa UmamiClient luokka, mutta sillä on myös joitakin haittoja . SeM SK2s UGLY-koodi ja tekee monenlaisia asioita, joita en tarvitse UmamiDataService Päätin yrittää toisenlaista lähestymistapaa.
UmamiDataServicen testaaminen
Euroopan unionin UmamiDataService on tuleva lisä Umamiin.Net-libraariin, joka antaa mahdollisuuden hakea tietoja Umamista asioihin kuten siihen, kuinka monta näkökohtaa sivulla oli Umami API toimii vain JavaScriptin kautta. Halusin siis leikkiä kyseisten tietojen kanssa ja pyrin luomaan sille C-clientin.
Euroopan unionin UmamiDataService luokka on jaettu moninkertaisiin osittaisiin luokkiin (menetelmät ovat SUPER pitkiäM SK1 esim. PageViews menetelmä.
Voitte nähdä, että suurin osa koodista rakentaa QueryString-aluetta PageViewsRequest -luokan välityksellä.
GetPageViews
public async Task<UmamiResult<PageViewsResponseModel>> GetPageViews(PageViewsRequest pageViewsRequest)
{
if (await authService.LoginAsync() == false)
return new UmamiResult<PageViewsResponseModel>(HttpStatusCode.Unauthorized, "Failed to login", null);
// Start building the query string
var queryParams = new List<string>
{
$"startAt={pageViewsRequest.StartAt}",
$"endAt={pageViewsRequest.EndAt}",
$"unit={pageViewsRequest.Unit.ToLowerString()}"
};
// Add optional parameters if they are not null
if (!string.IsNullOrEmpty(pageViewsRequest.Timezone)) queryParams.Add($"timezone={pageViewsRequest.Timezone}");
if (!string.IsNullOrEmpty(pageViewsRequest.Url)) queryParams.Add($"url={pageViewsRequest.Url}");
if (!string.IsNullOrEmpty(pageViewsRequest.Referrer)) queryParams.Add($"referrer={pageViewsRequest.Referrer}");
if (!string.IsNullOrEmpty(pageViewsRequest.Title)) queryParams.Add($"title={pageViewsRequest.Title}");
if (!string.IsNullOrEmpty(pageViewsRequest.Host)) queryParams.Add($"host={pageViewsRequest.Host}");
if (!string.IsNullOrEmpty(pageViewsRequest.Os)) queryParams.Add($"os={pageViewsRequest.Os}");
if (!string.IsNullOrEmpty(pageViewsRequest.Browser)) queryParams.Add($"browser={pageViewsRequest.Browser}");
if (!string.IsNullOrEmpty(pageViewsRequest.Device)) queryParams.Add($"device={pageViewsRequest.Device}");
if (!string.IsNullOrEmpty(pageViewsRequest.Country)) queryParams.Add($"country={pageViewsRequest.Country}");
if (!string.IsNullOrEmpty(pageViewsRequest.Region)) queryParams.Add($"region={pageViewsRequest.Region}");
if (!string.IsNullOrEmpty(pageViewsRequest.City)) queryParams.Add($"city={pageViewsRequest.City}");
// Combine the query parameters into a query string
var queryString = string.Join("&", queryParams);
// Make the HTTP request
var response = await authService.HttpClient.GetAsync($"/api/websites/{WebsiteId}/pageviews?{queryString}");
if (response.IsSuccessStatusCode)
{
logger.LogInformation("Successfully got page views");
var content = await response.Content.ReadFromJsonAsync<PageViewsResponseModel>();
return new UmamiResult<PageViewsResponseModel>(response.StatusCode, response.ReasonPhrase ?? "Success",
content ?? new PageViewsResponseModel());
}
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await authService.LoginAsync();
return await GetPageViews(pageViewsRequest);
}
logger.LogError("Failed to get page views");
return new UmamiResult<PageViewsResponseModel>(response.StatusCode,
response.ReasonPhrase ?? "Failed to get page views", null);
}
UmamiDataServicen testaaminen
Toisin kuin UmamiClient, -testissä, päätin testata UmamiDataService muttei käyttämällä Moq. Sen sijaan, luon yksinkertaisen DelegatingHandler luokka, joka antaa minulle mahdollisuuden tutkia pyyntöä ja palauttaa sen jälkeen vastauksen. Tämä on paljon yksinkertaisempi lähestymistapa kuin Moqin käyttö ja se antaa minulle tilaisuuden testata UmamiDataService ilman, että meidän on moittittava HttpClient.
jäljempänä olevassa koodissa voitte nähdä, että laajenn DelegatingHandler ja poistaa SendAsync menetelmä. Tällä menetelmällä voin tarkastaa pyynnön ja palauttaa sen perusteella vastauksen
public class UmamiDataDelegatingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var absPath = request.RequestUri.AbsolutePath;
switch (absPath)
{
case "/api/auth/login":
var authContent = await request.Content.ReadFromJsonAsync<AuthRequest>(cancellationToken);
if (authContent?.username == "username" && authContent?.password == "password")
return ReturnAuthenticatedMessage();
else if (authContent?.username == "bad")
{
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
else
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
default:
if (absPath.StartsWith($"/api/websites/{Consts.WebSiteId}/pageviews"))
{
var pageViews = GetParams<PageViewsRequest> (request);
return ReturnPageViewsMessage(pageViews);
}
if (absPath.StartsWith($"/api/websites/{Consts.WebSiteId}/metrics"))
{
var metricsRequest = GetParams<MetricsRequest>(request);
return ReturnMetrics(metricsRequest);
}
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
}
}
Järjestelmä
Uuden järjestelmän käyttöönotto UmamiDataService tämän käsittelijän käyttäminen on yhtä yksinkertaista.
public IServiceProvider GetServiceProvider (string username="username", string password="password")
{
var services = new ServiceCollection();
var mockLogger = new FakeLogger<UmamiDataService>();
var authLogger = new FakeLogger<AuthService>();
services.AddScoped<ILogger<UmamiDataService>>(_ => mockLogger);
services.AddScoped<ILogger<AuthService>>(_ => authLogger);
services.SetupUmamiData(username, password);
return services.BuildServiceProvider();
}
Näette, että olen juuri perustanut ServiceCollection, lisätään FakeLogger<T> (katso uudelleen Viimeinen artikkeli yksityiskohdista ja sen jälkeen perustaa UmamiData palvelu, johon sisältyy käyttäjänimi ja -tunnus, jota haluan käyttää (niten voin testata epäonnistumisen
Vetoan sitten services.SetupUmamiData(username, password); joka on laajentumismenetelmä, jonka olen luonut UmamiDataService neuvoston UmamiDataDelegatingHandler ja AuthService;
public static void SetupUmamiData(this IServiceCollection services, string username="username", string password="password")
{
var umamiSettings = new UmamiDataSettings()
{
UmamiPath = Consts.UmamiPath,
Username = username,
Password = password,
WebsiteId = Consts.WebSiteId
};
services.AddSingleton(umamiSettings);
services.AddHttpClient<AuthService>((provider,client) =>
{
client.BaseAddress = new Uri(umamiSettings.UmamiPath);
}).AddHttpMessageHandler<UmamiDataDelegatingHandler>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5)); //Set lifetime to five minutes
services.AddScoped<UmamiDataDelegatingHandler>();
services.AddScoped<UmamiDataService>();
}
Voitte nähdä, että tämä on se, missä kiinnitän UmamiDataDelegatingHandler ja AuthService neuvoston UmamiDataService. Järjestelmä on sellainen, että AuthService ' HttpClient ja UmamiDataService käyttää AuthService Umami API:hun tehtävien puhelujen tekemiseen bearer merkki ja BaseAddress jo asetettu.
Testit
Todellisuudessa tämä tekee itse asiassa testauksesta tämän todella yksinkertaisen. Se' on vain hieman kielellinen, koska minäkin halusin testata merkintää myösMSC2 Kaikki seM SK3s tekeminen on postittaminen minun DelegatingHandler ja simuloin vastauksen pyynnöstä.
public class UmamiData_PageViewsRequest_Test : UmamiDataBase
{
private readonly DateTime StartDate = DateTime.ParseExact("2021-10-01", "yyyy-MM-dd", null);
private readonly DateTime EndDate = DateTime.ParseExact("2021-10-07", "yyyy-MM-dd", null);
[Fact]
public async Task SetupTest_Good()
{
var serviceProvider = GetServiceProvider();
var umamiDataService = serviceProvider.GetRequiredService<UmamiDataService>();
var authLogger = serviceProvider.GetRequiredService<ILogger<AuthService>>();
var umamiDataLogger = serviceProvider.GetRequiredService<ILogger<UmamiDataService>>();
var result = await umamiDataService.GetPageViews(StartDate, EndDate);
var fakeAuthLogger = (FakeLogger<AuthService>)authLogger;
FakeLogCollector collector = fakeAuthLogger.Collector;
IReadOnlyList<FakeLogRecord> logs = collector.GetSnapshot();
Assert.Contains("Login successful", logs.Select(x => x.Message));
var fakeUmamiDataLogger = (FakeLogger<UmamiDataService>)umamiDataLogger;
FakeLogCollector umamiDataCollector = fakeUmamiDataLogger.Collector;
IReadOnlyList<FakeLogRecord> umamiDataLogs = umamiDataCollector.GetSnapshot();
Assert.Contains("Successfully got page views", umamiDataLogs.Select(x => x.Message));
Assert.NotNull(result);
}
}
Vastauksen simulointi
Toistaakseen tämän menetelmän vastauksen muistatte, että minulla on tämä rivi UmamiDataDelegatingHandler:
if (absPath.StartsWith($"/api/websites/{Consts.WebSiteId}/pageviews"))
{
var pageViews = GetParams<PageViewsRequest> (request);
return ReturnPageViewsMessage(pageViews);
}
Kaikki tämä tekee on vetää tietoa kysymyksen stringista ja rakentaa 'realistinen' vastaus (perustana Live-testeihin IM SK3ve compiledMSC4 jälleen hyvin vähän asiakirjoja tästäMST5 TeidänMSV6katsotte, että testaan päivämäärää lähtö- ja loppupäivämäärän välillä ja sitten palaan vastauksen saman päivämäärän kanssa
private static HttpResponseMessage ReturnPageViewsMessage(PageViewsRequest request)
{
var startAt = request.StartAt;
var endAt = request.EndAt;
var startDate = DateTimeOffset.FromUnixTimeMilliseconds(startAt).DateTime;
var endDate = DateTimeOffset.FromUnixTimeMilliseconds(endAt).DateTime;
var days = (endDate - startDate).Days;
var pageViewsList = new List<PageViewsResponseModel.Pageviews>();
var sessionsList = new List<PageViewsResponseModel.Sessions>();
for(int i=0; i<days; i++)
{
pageViewsList.Add(new PageViewsResponseModel.Pageviews()
{
x = startDate.AddDays(i).ToString("yyyy-MM-dd"),
y = i*4
});
sessionsList.Add(new PageViewsResponseModel.Sessions()
{
x = startDate.AddDays(i).ToString("yyyy-MM-dd"),
y = i*8
});
}
var pageViewResponse = new PageViewsResponseModel()
{
pageviews = pageViewsList.ToArray(),
sessions = sessionsList.ToArray()
};
var json = JsonSerializer.Serialize(pageViewResponse);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
Päätelmissä
Näin ollen on todellakin hyvin yksinkertaista testata HttpClient pyyntöä käyttämättä Moq-ohjelmaa, ja uskon sen olevan parempi.' on tällä tavoin paljon puhtaampi.