Cosa testare
- Prima fare i test nominali
- Testare i casi estremi (come le date limite di un periodo)
- Testare le exception (preferire exception castomizzate)
Caratteristiche generarli
- Vantaggi di progettare test automatici =>
- Testare il codice in meno tempo e più facilmente.
- Trovare eventuali bug e migliorare la qualità del codice.
- Fare il deploy e refactoring più serenamente.
- Tipi di test automatici =>
- Manual tests
- Unit Test => testano un’unità di un’applicazione (una classe o più classi) in modo isolato, senza cioè dipendenze esterne e automatico.
- Economici da scrivere
- Permettono di riscrivere il codice senza troppa paura
- Veloci da eseguire
- Non completamente affidabili
- Da fare e lanciare ripetutamente prima di committare le proprie modifiche nel repository
- Ogni dipendenza esterna deve essere un fake
- Riducono del bug fixing
- Possono fornire della documentazioni => se gli unit test seguono lo schema degli use-cases possono fornire un modo per documentare l’applicazione
-
- Integration Test (API test) => testano un’unità di un’applicazione (o componente) insieme alle sue dipendenze esterne.
- Sono più lunghi da scrivere
- Più affidabili
- Da fare solo dopo aver committato le modifiche nel repository
- End-to-End Test (GUI Test) => basati sull’interfaccia utente
- Molto affidabili
- Molto lenti (fare il login/ aprire la pagina da testare / verificare il risultato)
- Sono molto fragili => un piccolo cambiamento nell’interfaccia potrebbero inficiare i test
- Acceptance tests =>
- Testano il comportamento atteso
- Performance/Load/Volume tests
- Test a piramide => E’ un metodo che raggruppa i 3 metodi precedenti (vedi immagine)
- Integration Test (API test) => testano un’unità di un’applicazione (o componente) insieme alle sue dipendenze esterne.
-
-
- Step 1 => Scrivere Unit test per testare il grosso dell’applicazione. Soprattutto per testare algoritmi e il grosso della logica
- Step 2 => Se non si è abbastanza confidenti, scrivere dei test d’integrazione che coinvolgono anche le dipendenze esterne della nostra applicazione.
- Step 3 => Infine scrivere alcuni test per verificare l’interfaccia sulle principali funzionalità dell’applicativo, con parsimonia.
-
TDD (Test-Driven Development) or Test-First (Vs Code-First)
- E’ un modo di programmare dove si scrivono prima i test e poi l’applicazione vera e propria.
- Implementazione a tre fasi =>
-
- In primis si scrive un test che fallisce.
- Poi si scrive il codice più semplice affinché il test risulti positivo.
- Fare il refactoring del codice dove necessario per renderlo più affidabile e mantenibile
- Pros =>
- Test coverage => Tutta l’applicazione è pienamente testata.
- Clean design =>
- Spesso l’implementazione è più semplice.
- Si è sicuri che il codice è testabile senza nessun cambiamento
- Focus on requirement => Si punta l’attenzione sulle esigenze e non sull’implementezione
Testing Frameworks (Libraries + Test runner)
- Principali framework per testare applicazione in c# =>
- NUnit (uno dei primi)
- Attributo per classi => [TestFixture]
- Attributo per metodi => [Test]
- Assert =>
-
Assert.That(result, Is.True); or Assert.That(result == true);
Assert.That(result, Is.False); or Assert.That(result == false);
Assert.That(result.Count(), Is.EqualTo(..))
//Deal with string
Assert.That(result, Is.EqualTo("...").IgnoreCase)
Assert.That(result, Does.StartWith("...").IgnoreCase)
Assert.That(result, Does.EndWith("...").IgnoreCase)
Assert.That(result, Does.Contain("..."))
//Deal with array/collection
Assert.That(result, Is.Not.Empty)
Assert.That(result.Count(), Is.EquivalentTo(new [] {1,2,3}))
Assert.That(result, Is.Ordered)
Assert.That(result, Is.Unique)
//Testing return type
//Verifica se result sia esattamente un'istanza del tipo indicato
Assert.That(result, Is.TypeOf<...>())
//Verifica se result sia esattamente un'istanza del tipo indicato
//o un'instanza di eventuali classi derivate da quest'ultimo
Assert.That(result, Is.InstanceOf<...>())
//Testing an exception
Assert.That(() => [metodo_da_testare], Throws.ArgumentNullException);
Assert.That(() => [metodo_da_testare], Throws.Exception.TypeOf<..>());
//Testing method that raising event
//Prima di eseguire il metodo è sufficente implementare l'evento
//e dopo verificarne la corretta esecuzione
-
- Nelle versioni precedenti a Visual Studio 2019 per aver NUnit bisogna installare i package:
-
Install-Package NUnit
-
Install-Package NUnit3TestAdapter [-Version 3.8.0]
-
- MS Test (built-in in Visual Studio)
- Attributo per classi => [TestClass]
- Attributo per metodi => [TestMethod]
- Assert =>
-
Assert.IsTrue(result);
Assert.IsFalse(result);
-
- xUnit (in voga negli ultimi anni) =>
- Creare nuovo progetto con la shell =>
-
//posizionarsi con la linea di comando nella cartella che ospiterà l'applicazione di test
dotnet new xunit -n <my-test-project-name>
//aggiungo la referenze al progetto da testare
dotnet add reference <projectfile-path>
//aggiungo la referenze a mop
dotnet add package moq
-
- Attributo per metodi => [Fact]
- Assert =>
-
var item = Assert.Single(<some_ienumerable>);
Assert.NotNull(item);
Assert.Equal("pippo", item);
Assert.True(<my_value>);
Assert.False(<my_value>);
result.ShouldBeAssignableTo<ViewResult>();
-
- Creare nuovo progetto con la shell =>
- NUnit (uno dei primi)
- Testing product (sembrano essere stati inglobati in VS 2022) =>
- ReSharper
- Rider
- ReSharper.DotCover
1. Unit-Tests
- Good practices =>
- I test sono importanti come i codice vero e proprio.
- Un test deve avere una sola e unica responsabilità.
- Non devono contenere della logica (es: if statement).
- Né troppo generico, né troppo specifico.
- Non fare copia incolla del codice da testare nei metodi del progetto di test
- Piuttosto usare le funzionalità offerte dal framework scelto per i test come client per interagire con le funzionalità da testare
- Cosa testare =>
- Se la routine ritorna un valore, testare che sia quello desiderato per ognuno dei possibili percorsi d’esecuzione all’interno della routine stessa.
- In MVC se il controller ha una sola linea di codice (come in un’architettura DDD) non c’è bisogno di testarlo
- Il business layer ha bisogno di unit test
- Testare solo il proprio codice => non testare codice esterno non scritto da te, as es: EF o ASP.NET
- Testare l’iterazione tra oggetti
- Validare se le chiamate ai componenti esterni sono state effettuate
- Non è necessario validare se i componenti esterni hanno ben funzionato (saranno gli intergration test a farlo)
- Testare il cambio di stato dei componenti
- Significa testare ciò che ha cambiato di valore
- Cosa non testare =>
- Caratteristiche del linguaggio.
- Codice di terzi.
- Metodi private o protected.
- Organizzazione del progetto di test =>
- Ogni test avrà 3 fasi (Metodo della tripla A)
- Arrange => istanziare il metodo da testare
- Act => eseguire il metodo da testare
- Assert => verificare l’esecuzione
- Per ogni progetto della soluzione creare un progetto di test.
- Separare il progetto secondo le componenti del progetto da testare
- Ogni test avrà 3 fasi (Metodo della tripla A)
-
- Per ogni classe di un progetto creare una classe di test.
- Per ogni metodo di una classe creare un metodo nella corrispondente classe di test.
- Numero di tests >= numero di percorsi di esecuzione del metodo da testare
- Creare i metodi vuoti per ognuno dei percorsi da testare e poi iniziarne l’implementazione.
- Se un metodo è particolarmente complicato e con molti percorsi da testare è possibile creare una classe ad hoc.
- Ogni metodo deve essere isolato.
- Testare ogni return presente nella routine/funzione
- Validare ogni edge case
- Black Box => Non bisogna realizzare il metodo di test in funzione dell’implementazione del metodo da testare ma della funzione che deve realizzare.
- Trustworthy = > E’ possibile aumentare la confidenza verso il metodo di test che abbiamo scritto testandolo nel seguente modo:
- Creare un bug nel metodo che si sta testando.
- Lanciare il metodo di test che abbiamo scritto.
- Verificare che il metodo di test fallisca. Se non fallisce, il metodo di test ha un bug.
- NUnit =>
- [TestCases] => E’ possibile creare un metodo di test con parametri e eseguirlo più volte decorandolo con [TestCases]. Ciò permette di centralizzare l’esecuzione di test simili.
- [SetUp] => E’ possibile creare un metodo decorato con [SetUp] che verrà eseguito prima di ogni test.
- [TearDown] (spesso usato nei test di integrazione) => E’ possibile creare un metodo decorato con [TearDown] che verrà eseguito dopo di ogni test.
- NUnit e MSTest =>
- [Ignore] => E’ possibile disabilitare temporaneamente un metodo e saltarne l’esecuzione aggiungendo al metodo l’attributo [Ignore(“descrizione del motivo per cui ho disabilitato il test”)]
- Naming convention =>
- Nome progetto per i test => [nome_progetto_da_testare].UnitTests
- Nome classi => [nome_classe_da_testare]Tests
- Nome metodi => (3 parti) [Component_to_test]_[Scenario]_[ExpectedBehavior or return value]_[possible Parameters]
- Scenario =>
- Scrivere un metodo di test per ognuno dei possibili percorsi di esecuzione
- WhenCalled => usarlo quando si vuole testare il solo percorso possibile
- ExpectedBehavior =>
- ReturnsTrue
- ReturnsFalse
- ReturnsTheSumOfArguments
- Esempi =>
-
//Homecontroller => componente
//Index => action o scenario
//ShouldReturnView => comportamento o valore atteso
HomeController_Index_ShouldReturnView()
//stesso caso di sopra ma con parametro in ingresso
//WithNullId => attendo un parametro in ingresso
HomeController_Index_ShouldReturnView_WithNullId()
GetItemAsync_WithUnexistingItem_ReturnNotFound
-
- Scenario =>
2. Integration Tests
- Intera pipeline => Servono per testare l’intero ciclo della mia web app, dall’inizio delle http request alla fine della http response:
- routing
- controllers
- actions
- middlelayer
- database
- Ogni test deve essere eseguito in isolazione e indipendente dagli altri
- Usare oggetti fake il meno possibile
- fake le chiamate al db
- fake le chiamate a provider esterni (es: provider di pagamento, provider di geolocalizzazione
- non fake i componenti interni del nostro sistema
- Possono avere delle parti fake (es: database) ma la maggior parte dei componenti esterni non devono essere fake
- Rompere le dipendenze esterne => Dependency Injection (DI)
- Per poter unit-testare del codice che abbia delle interazioni con risorse esterne è necessario rompere queste dipendenze ottenendo così un ambiente debolmente accoppiato =>
- Eseguire le seguenti 3 operazioni:
- Isolare le classi del nostro codice che dialogano con l’esterno =>
- Cercare ad esempio i vari punti dove si istanziano delle nuove classi.
- Estrarre da queste classi delle interfacce.
- Modificare le altre classi del nostro codice affinché usino tali interfacce.
- Sostituire cioè le istanziazioni con il passaggio di interfacce.
- Isolare le classi del nostro codice che dialogano con l’esterno =>
- A questo punto il nostro codice non dipenderà più da una implementazione specifica ma da un’interfaccia =>
- In un ambiente di test verranno passate delle implementazioni fake (mock o stub) di tali interfaccie.
- Eseguire le seguenti 3 operazioni:
- Tre modi per implementare la Dependency Injection =>
- Instanziazione nel costruttore =>
- utilizzo di una framework per DI
- Passaggio come parametro =>
- necessita di riscrivere la signature del metodo ovunque questa è usata.
- non tutti i framework che gestiscono la DI gestiscono questo tipo di ingezione.
- Passaggio tramite poprietà
- Instanziazione nel costruttore =>
- Per poter unit-testare del codice che abbia delle interazioni con risorse esterne è necessario rompere queste dipendenze ottenendo così un ambiente debolmente accoppiato =>
- Dependency Injection Framework =>
- Nella maggior parte dei DI framework esiste un container dove sono registrate tutte le interfacce e le loro implementazioni concrete.
- Quando l’applicazione il framework è responsabile di inizializzare le giuste dipendenze per ogni oggetto pescando le corrispondenze dal container.
- I più diffusi =>
- NInject
- Autofac
- Azure (CI/CD) =>
- Testare web api che accede ad un db sql server tramite EF.
- Progetto di test =>
- Aggiungere la referenza al progetto web api che si vuole testare.
- appsetting.json =>
- Il progetto di test ha il proprio file appsetting.json in modo da poter configurarli con la massima libertà
- Ad esempio è possibile modificare la stringa id conessione e decidere di eseguire i test un un’altro DB
- DbContext factory =>
- Il progetto di test per poter testare la connessione al DB deve istanziare il DbContext
- Aggiungere il package Microsoft.Extensions.Configuration.Json
- Aggiungere il package ‘EntityFrameworkCore.SqlServer’
-
using Microsoft.Extensions.Configuration;
//creare il DbContext
var factory = new CookBookContextFactory();
using var CookBook = factory.CreateDbContext(args);
//abbiamo deciso di lanciare il test su un nuovo database
//(semplicemente customizzando l'appsettings.json del progetto di test)
//quindi dobbiamo assicurarci di crearlo prima di usarlo.
//per evitare una doppia creazione, prima lo elimino
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
//Se decido di rimuovere e ricreare il Db ad ogni lancio di test
//allora posso saltare il prossimo passaggio (eliminazione customer eventualmente già esistenti)
//Delete all existing customer
//context.Customers!.RemoveRange(await context.Customers.ToArrayAsync());
//await context.SaveChangesAsync();
//Factory class for retrieving DbContext
class MyDbContextFactory: IDesignTimeDbContextFactory<MyDbContext>
{
public <MyDbContext> CreateDbContext(string[]? args = null)
{
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var optionBuilder = new DbContextOtionsBuilder<MyDbContext>();
optionBuilder
.UseSqlServer(configuration["ConnectionStrings:DefaultConnection"]);
return MyDbContext(optionBuilder.options);
}
}
- Testare il controller =>
- Aggiungere il package ‘AspNetCore.Mvc.Core’
-
//Create customer controller
var controller = new CustomersController(context);
//Add customer
var customer = new Customer() { Name = "prova" };
await controller.Add(customer);
//Check does the GetAll return the added customers
var result = (await controller.GetAll()).ToArray();
//XUnit syntax
Assert.Single(result);
Assert.Equal("prova", result[0].Name);
- Mocking Framework =>
- Quando usare classi mock =>
- Per testare dipendenze esterne
- Per testare una specifica classe in scenari in cui più classi sono chiamate insieme.
- Moq (instalare tramite NuGet) =>
- Verify() method =>
- Per fare dei test d’integrazione si usa spesso l’opzione Verify di Moq. Cioè verifico che la routine del componente esterno sia stata chiamata con i parametri d’ingresso desiderati.
- Oppure che non sia stata chiamata (usando Times.Never)
- Object property => Una volta inizializzato l’ogetto Mock per recuperarne un’istanza è necessario usare la proprietà object
-
var fileReaderMoq = new Mock<IFileReader>();
IFileReader myfileReaderObj = fileReaderMoq.Object;
-
- Setup => E’ possibile inizializzare il comportamento di un dato metodo nella classe moq
-
//la classe moq è inizializzata affinché il metodo Read() per ogni stringa in ingresso ritorni stringa vuota ("")
fileReaderMoq.Setup(x => x.Read(It.IsAny<string>())).Returns("");
-
- Stub vs Moq => Se non uso il metodo .Verify posso chiamare le mie variabili con il suffisso stub se no posso usare moq
-
- caso1 => è il caso non uso il metodo fileReaderStub.Verify(...)
var fileReaderStub = new Mock<IFileReader>();
- caso 2 => uso il metodo fileReaderStub.Verify(...)
var fileReaderMoq = new Mock<IFileReader>()
-
- Run test =>
- Tramite console =>
- Verify() method =>
- Quando usare classi mock =>
-
-
-
- .NET Core Test Explorer for VS Code => Direttamente dalla metodo di test
-
-
-
-
- Esempio classe (IFileReader) =>
-
public class MyClass
{
string Name {get; set;}
}
public class MyTestWithMoq
{
MyClass myClass = new { Name = "prova" };
-- step1 - creazione di un oggetto Mock per simulare l'interfaccia IFileReader
var fileReaderMoq = new Mock<IFileReader>();
-- step2 - inizializzazione dell'oggetto Mock
//quando il metodo Read viene chiamato con la stinga "video.txt" come parametro deve ritornare ""
fileReaderMoq.Setup(x => x.Read("video.txt")).Returns("");
fileReaderMoq.Setup(x => x.Read(It.IsAny<string>())).Returns("");
fileReaderMoq.Setup(x => x.Read(It.IsAny<myClass>())).Returns("prova");
//caso asincrono
fileReaderMoq.Setup(x => x.Read(It.IsAny<myClass>())).ReturnsAsync("prova");
//faccio in modo che il metodo ReadAll lancia un'eccezione
fileReaderMoq.Setup(x => x.ReadAll(It.IsAny<string>()).Throws<Exception>();
-- step 3 - verifico che l'eccezione sia stata ben lanciata
Assert.That(result, Throws.TypeOf<Exception>());
Assert.IsType<NotFoundResult>(myResult);
//utilizzo dell'oggetto Mock nel costruttore di VideoService
var videoService = new VideoService(fileReaderMoq.Object);
-- Verify
//verifico che il metodo Store sia stato effettivamente chiamato
fileReaderMoq.Verify(s => s.Store(video));
//verifico che il metodo Store non venga mai chiamato
fileReaderMoq.Verify(s => s.Store(video), Times.Never);
//posso che il metodo Store sia chiamato una volta
fileReaderMoq.Verify(s => s.Store(video), Times.Once);
}
-
- Esempio Controller – Test Get item =>
- Action da testare – GetItemAsync(Guid id):
- Esempio classe (IFileReader) =>
-
-
-
-
- Caso item is null – GetItemAsync_WithUnexisitingItem_ReturnsNotFound():
-
-
-
-
-
-
- Usano Fluent Assertion library l’Assert:
-
-
-
-
-
-
- Caso item is not null – GetItemAsync_WithExisitingItem_ReturnsExpectedItem():
-
-
-
-
-
-
- Usano Fluent Assertion library l’Assert diventa:
-
-
-
-
-
- Esempio Controller – Test a collection =>
- Action da testare – GetItemsAsync():
- Esempio Controller – Test a collection =>
-
-
-
-
- Caso return all items – GetItemsAsync_WithExisitingItem_ReturnsAllItems():
-
-
-
-
- Esempio Controller – Test post =>
- Action da testare – CreateItemAsync(CreateItemDto item):
- Esempio Controller – Test post =>
-
-
-
-
- Test creazione – CreateItemAsync_WithItemToCreate_ReturnCreatedItem():
-
-
-
-
- Esempio Controller – Test update =>
- Action da testare – UpdateItemAsync(Guid id, UpdateItemDto item):
- Esempio Controller – Test update =>
-
-
-
-
- Test update – UpdateItemAsync_WithExisitingItem_ReturnsNoContent():
-
-
-
-
- Esempio Controller – Test Delete =>
- Action da testare – DeleteItemAync(Guid id)
- Esempio Controller – Test Delete =>
-
-
-
-
- Test delete:
-
-
-
- Altri Moq framework =>
- NSubstitute
- FateItEasy
- Rhyno Mocks
- Altri Moq framework =>
- Fluent Assertion library =>
- Permette di fare Assert.Equal in mdo automatico per tutte le proprietà dell’oggetto da comparare
- Installare package: