Interfaces om mee te testen
Interfaces
In object geörienteerde programmeertalen kan je gebruikmaken van interfaces. Interfaces zijn een hulpmiddel voor ontwikkelaars. Ze geven de ontwikkelaar aan wat een bepaalde klasse kan of moet doen (in termen van invoer en uitvoer), maar niet hoe dat moet gebeuren. Om deze reden zijn ze handig (wellicht zelfs essentiëel) als het gaat om: testen, uitbreiden en beheren. Hieronder zal ik een voorbeeld geven hoe je interfaces kan gebruiken om je tests eenvoudiger en acurater uit te voeren.
Ik heb hieronder een klein programmaatje staan dat een review wegschrijft naar een locatie. Het is bepaald geen code om te gebruiken in productie, maar dient slechts als voorbeeld.
static void Main(string[] args)
{
var review = new Review()
{
Name = "Dominique",
Date = DateTime.Parse("23-05-2020"),
Message = "Deze blog laat goed zien hoe je interfaces kan gebruiken om je tests eenvoudiger en accurater te maken!"
};
storeReview(review);
Console.WriteLine("Review bewaard.");
Console.ReadLine();
}
private static void storeReview(Review review)
{
string parsedReviewContent = $"{review.Name} op {review.Date.ToShortDateString()}: {review.Message}";
parsedReviewContent += "\n";
System.IO.File.AppendAllText("c:\\hierbewaarikreviews\\reviews.txt", parsedReviewContent);
}
public class Review
{
public string Name {get; set;}
public DateTime Date {get; set;}
public string Message {get; set;}
}
Als ik deze code wil testen, dan moet ik dit programma uitvoeren en kijken of de review is weggeschreven naar het betreffende bestand. Ik heb dat zojuist gedaan: deze code werkt. Maar er is hier een probleem: omdat ik in de code expliciet gebruikmaak van System.IO.File, test ik niet alleen de code die ik heb geschreven en die ik wil testen, ik test ook nog eens een externe bron, namelijk de harde schijf. (Er zijn wel meer problemen met deze code, zoals de statische verwijzing naar reviews.txt, maar die zijn in dit voorbeeld niet relevant.)
Externe bronnen of verwijzingen beschouw ik hier als alle aanroepen naar functies of methodes of wat dan ook, die niet worden (of kunnen) uitgevoerd op een theoretische dezelfde processor als je programma zelf, maar die ook gebruik maken van bijvoorbeeld: de harde schijf, het netwerk (internet) of een databaseserver. Deze bronnen kunnen stuk gaan (de hardeschijfruimte is vol of een netwerkverbinding wordt geweigerd) en wanneer ze tijdens het testen stuk gaan of onverwacht gedrag vertonen, faalt de test - terwijl er niets mis is met je code!
Interfaces kunnen helpen om dat probleem op te lossen. In bovenstaande geval zou ik de aanroep naar System.IO.File kunnen aanpassen en vervangen door een aanroep naar een abstracte klasse, waarvan ik het gedrag zelf bepaal. Ik doe dit in vier stappen (waarvan de volgorde overigens niet relevant is):
Eerst pas ik de storeReview-methode aan zodat ik gebruik maak van abstracte logica en geen specifieke implementatie:
private static void storeReview(Review review, IFileSystem fileSystem)
{
string parsedReviewContent = $"{review.Name} op {review.Date.ToShortDateString()}: {review.Message}";
parsedReviewContent += "\n";
fileSystem.AppendContent("c:\\hierbewaarikreviews\\reviews.txt", parsedReviewContent);
}
Daarna maak ik een interface aan die ik IFileSystem noem:
public interface IFileSystem
{
void AppendContent(string path, string content);
}
Zoals je ziet heb ik de methode een andere naam gegeven dan de methode in System.IO.File die ik eerste gebruikte. Dit kan.
Het aanroepen van storeReview moet ik daarna aanpassen, zodat er een implementatie van IFileSystem wordt meegestuurd en daarna dien ik die implementatie ook nog eens te maken:
IFileSystem fileSystem = new VirtualFileSystem();
storeReview(review, fileSystem);
private class VirtualFileSystem : IFileSystem
{
private Dictionary<string, string> _files;
public VirtualFileSystem()
{
_files = new Dictionary<string, string>();
}
public void AppendContent(string path, string content)
{
var existingFileContent = "";
if (_files.ContainsKey(path))
{
existingFileContent = _files[path];
}
existingFileContent += content;
_files[path] = existingFileContent;
}
Nu kan ik deze code uitvoeren en testen. De koppeling met de harde schijf is verdwenen, dus ik test nu slechts de code van storeReview en dat is het enige wat ik wil testen. De implementatie van IFileSystem kan natuurlijk fouten bevatten waardoor de test niet succesvol is, maar ik heb controle over de implementatie en kan de fouten dus gemakkelijk, in code, rechtzetten.
Maar hoe test ik dit nu? Er is immers geen bestand om te controleren. In dit geval voeg ik een nieuwe functie doe aan de interface, die ik implementeer:
string ReadFileAsString(string path);
// In VirtualFileSystem:
public string ReadFileAsString(string path)
{
return _files[path];
}
Nu kan ik de inhoud van een bestand (virtueel of hoe dan ook) lezen. Hier maak ik gebruik van om te testen of een nieuwe review daadwerkelijk wordt toegevoegd aan bestaande en deze niet overschrijft:
var review = new Review()
{
Name = "Dominique",
Date = DateTime.Parse("23-05-2020"),
Message = "Deze blog laat goed zien hoe je interfaces kan gebruiken om je tests eenvoudiger en accurater te maken!"
};
var review2 = new Review()
{
Name = "Dominique",
Date = DateTime.Parse("24-05-2020"),
Message = "Goed voorbeeld, maar statische verwijzingen?!"
};
IFileSystem fileSystem = new VirtualFileSystem();
storeReview(review, fileSystem);
storeReview(review2, fileSystem);
var content = fileSystem.ReadFileAsString("c:\\hierbewaarikreviews\\reviews.txt");
Console.WriteLine("Review bewaard:");
Console.WriteLine(content);
Console.ReadLine();
Het resultaat is wat ik verwacht, de code werkt:
Review bewaard:
Dominique op 23-5-2020: Deze blog laat goed zien hoe je interfaces kan gebruiken om je tests eenvoudiger en accurater te maken!
Dominique op 24-5-2020: Goed voorbeeld, maar statische verwijzingen?!
En het wegschrijven naar een fysiek bestand, hoe zit dat nu? Gewoon een implementatie maken van de interface IFileSystem en daar gebruikmaken van de juiste methodes:
IFileSystem fileSystem = new ActualFileSystem();
// Nieuwe implementatie:
public class ActualFileSystem : IFileSystem
{
public void AppendContent(string path, string content)
{
System.IO.File.AppendAllText(path, content);
}
public string ReadFileAsString(string path)
{
return System.IO.File.ReadAllText(path);
}
}
Wat is het resultaat als ik deze nu drie keer uitvoer? In ieder geval een bestand reviews.txt en de betreffende reviews er drie keer in staan:
Review bewaard:
Dominique op 23-5-2020: Deze blog laat goed zien hoe je interfaces kan gebruiken om je tests eenvoudiger en accurater te maken!
Dominique op 24-5-2020: Goed voorbeeld, maar statische verwijzingen?!
Dominique op 23-5-2020: Deze blog laat goed zien hoe je interfaces kan gebruiken om je tests eenvoudiger en accurater te maken!
Dominique op 24-5-2020: Goed voorbeeld, maar statische verwijzingen?!
Dominique op 23-5-2020: Deze blog laat goed zien hoe je interfaces kan gebruiken om je tests eenvoudiger en accurater te maken!
Dominique op 24-5-2020: Goed voorbeeld, maar statische verwijzingen?!
Dit toont een ander voordeel aan van interfaces tijdens het testen: je kan de omstandigheden zelf beĂŻnvloeden. In mijn testopstelling bestond reviews.txt niet, maar ik had deze al aan kunnen maken in VirtualFileSystem. Ik zou er ook voor kunnen zorgen dat mijn implementatie een Exception teruggeeft bij het aanroepen van AppendContent, bijvoorbeeld dat de schijfruimte vol is, en mijn code daarmee om kunnen laten gaan.