C# Tips & Tricks: Hierarchical tests

Jakiś czas temu miałem przyjemność pisać webową aplikację w stylu SPA.
Wybór technologii zakończył się na JavaScript + AngularJs + WebAPI.

Nic nowego, standardowy stos i aplikacja…
Jednakże, dla mnie było to pierwsze spotkanie z testami jednostkowymi w JavaScriptcie.

A dokładniej z frameworkiem: Jasmine

Jasmine zowie się frameworkiem testowym o smaku Behavior-Driven.

Wygląda to mniej więcej tak:

describe("Validator", function () {
	var input;
	var validator;

	beforeEach(function () {
		input = {};
		validator = new Validator();
	});

	describe("when email do not exist", function () {
		it("validation fails", function () {
			expect(validator.validate(input)).toEqual({
				isSuccess: false,
				errorMessage: 'Missing email'
			});
		});
	});

	describe("when email exist", function () {
		beforeEach(function () {
			input.email = ''
		});

		describe("when email is empty", function () {
			it("validation fails", function () {
				expect(validator.validate(input)).toEqual({
					isSuccess: false,
					errorMessage: 'Empty email'
				});
			});
		});

		describe("when email has incorrect format", function () {
			beforeEach(function () {
				input.email = 'incorrect-format'
			});
			
			it("validation fails", function () {
				expect(validator.validate(input)).toEqual({
					isSuccess: false,
					errorMessage: 'Incorrect email'
				});
			});
		});

		describe("when email has correct format", function () {
			beforeEach(function () {
				input.email = 'correct@email.com'
			});
			
			it("validation succeeds", function () {
				expect(validator.validate(input)).toEqual({
					isSuccess: true
				});
			});
		});
	});
});

A co w tym jest takiego ciekawego?

A no, pomysł na zagnieżdżanie testów.

Hierarchia w testach

Jeżeli mamy kod który posiada strukturę drzewiastą (np. walidacja) to często będziemy powtarzać etap przygotowania stanu przed samym testem.

Spójrzmy na standardowy przykład testów w C#:

public class ValidatorTests 
{
    private Validator _validator;

	[SetUp]
	public void SetUp()
	{
		_validator = new Validator();
	}
	
	[Test]
	public void WhenEmailIsNull_ValidationFails()
	{
		string email = null;
		var result = _validator.Validate(email);
		Assert.IsFalse(result.IsSuccess);
	}
	
    [Test]
	public void WhenEmailIsEmpty_ValidationFails()
	{
		string email = "";
		var result = _validator.Validate(email);
		Assert.IsFalse(result.IsSuccess);
	}
	
    [Test]
	public void WhenEmailHasIncorrectFormat_ValidationFails()
	{
		string email = "Incorrect";
		var result = _validator.Validate(email);
		Assert.IsFalse(result.IsSuccess);
	}
	
	[Test]
	public void WhenEmailHasCorrectFormat_ValidationSucceeds()
	{
		string email = "correct@email.com";
		var result = _validator.Validate(email);
		Assert.IsTrue(result.IsSuccess);
	}
}

a teraz na wersję z zagnieżdżeniem (warto zwrócić uwagę na hierarchię dziedziczenia):

[TestFixture]
public class ValidatorTests 
{
	private string _email = null;
	private Validator _validator;

	[SetUp]
	public void SetUp()
	{
		_validator = new Validator();
	}

	[TestFixture]
	public class WhenEmailIsNull :  ValidatorTests
	{
		[Test]
		public void ValidationFails()
		{
			var result = _validator.Validate(_email);
			Assert.IsFalse(result.IsSuccess);
		}
	}	

	[TestFixture]
	public class WhenEmailIsEmpty :  ValidatorTests
	{
		[SetUp]
		public void SetUp()
		{
			_email = "";
		}
	
		[Test]
		public void ValidationFails()
		{
			var result = _validator.Validate(_email);
			Assert.IsFalse(result.IsSuccess);
		}				
	}

	[TestFixture]
	public class WhenEmailIsNotEmpty :  ValidatorTests
	{
        [TestFixture]
		public class WhenEmailHasIncorrectFormat :  WhenEmailIsNotEmpty
		{
			[SetUp]
			public void SetUp()
			{
				_email = "incorrect";
			}
		
			[Test]
			public void ValidationFails()
			{
				var result = _validator.Validate(_email);
				Assert.IsFalse(result.IsSuccess);
			}				
		}

		[TestFixture]
		public class WhenEmailHasCorrectFormat :  WhenEmailIsNotEmpty
		{
			[SetUp]
			public void SetUp()
			{
				_email = "correct@email.com";
			}
		
			[Test]
			public void ValidationSucceeds()
			{
				var result = _validator.Validate(_email);
				Assert.IsTrue(result.IsSuccess);
			}				
		}		
	}
	
}

Porównajmy również raport z R#:

Bez hierarchii
Bez hierarchii
Z hierarchią
Z hierarchią

Oczywiście, im więcej mamy rozgałęzień w logice oraz im bardziej skomplikowany kod w środku – tym więcej będziemy w stanie zmieścić w zagnieżdżonych setupach.

Kiedy stosować?

  • Metody setup zaczynają przybierać na liniach
  • Struktura kodu przypomina drzewo
  • Kiedy w metodach testów pojawiają się słowa typu: “when”, “then” itp
  • Kiedy chcemy mieć bardziej czytelne kolejne kroki algorytmów (w testach)

Inne przypadki

Testy integracyjne często układają się w drzewo-podobne struktury.
Np. w testach zapytań SQL – Join z kilku tabelek zwykle wygląda tak:

SELECT TableA.*, TableB.*, TableC.*
FROM TableA
JOIN TableB ON TableB.aID = TableA.aID
JOIN TableC ON TableC.cID = TableB.cID

Więc nasze drzewo testów będzie miało formę:

  • When records in table A do not exist
    • Query returns empty list
  • When records in table A do exist
    • When records in table B do not exist
      • Query returns empty list
    • When records in table B do exist
      • When records in table C do not exist
        • Query returns empty list
      • When records in table C do exist
        • Query returns selected records

Pisanie metody pokroju:

WhenTableAHasRecords_And_TableBHasRecords_And_TableCHasRecords_RecordsAreReturned

raczej nie należy do najprzyjemniejszych, a próba jej przeczytania przez innego programistę prędzej skończy się na tępych odgłosach konsultacji głowa-biurko niż słowach afirmacji.

Be First to Comment

A penny for your thoughts