There are more to unit testing than using a unit testing framework - in order to succeed you want to use the right tools for the job. There are a few tools that almost no one talks about - some enabling creating of top-notch, robust unit tests. Some will help you run your tests better and faster.
In this session I'll explain about the inevitable maintainability problems developers face when writing and maintaining huge unit testing suits and how unit level BDD, AutoMocking, and Continuous Execution can help take control over your tests.
9. A good Unit Test should be:
Easy
to write
Simple
to read
Trivial
to maintain
10. What about AAA?
[Test]
public async void GetUserFromUrl() {
var clientFake = A.Fake<IJsonClient>();
A.CallTo(() => clientFake.HttpGetUncompressedAsync(A<string>.Ignored))
.Returns(Task.FromResult(JsonResult));
var userRepository = new UserRepository(clientFake);
var user = await userRepository.GetUser(11361);
var expected = new User {
Id=11361, DisplayName = "Dror Helper", ImageUrl=DefaultAvatar, Reputation=13904
};
Assert.That(user, Is.EqualTo(expected));
}
Arrange
Act
Assert
12. Arrange issues
[TestMethod]
public async Task LoadUser_ReputationStaysTheSame_ReputationTrendSame() {
var user1 = new User {ImageUrl = "http://dummy.jpg", Reputation = 10};
var user2 = new User {ImageUrl = "http://dummy.jpg", Reputation = 10};
var fakeUserRepository = new FakeUserRepository(new[] { user1, user2 });
var viewModel = await InvokeAsync(() => new UserDetailsViewModel(fakeUserRepository));
await viewModel.LoadUser();
await viewModel.LoadUser();
var result = await InvokeAsync(() => ((SolidColorBrush)viewModel.ReputationTrend).Color);
Assert.AreEqual(Colors.White, result);
}
13. [TestInitialize]
public async Task InitializeViewModel() {
var user1 = new User {ImageUrl = "http://dummy.jpg", Reputation = 10};
var user2 = new User {ImageUrl = "http://dummy.jpg", Reputation = 10};
var fakeUserRepository = new FakeUserRepository(new[] { user1, user2 });
_viewModel = await InvokeAsync(() => new UserDetailsViewModel(fakeUserRepository));
}
[TestMethod]
public async Task LoadUser_ReputationStaysTheSame_ReputationTrendSame() {
await _viewModel.LoadUser();
await _viewModel.LoadUser();
var result = await InvokeAsync(() => ((SolidColorBrush)_viewModel.ReputationTrend).Color);
Assert.AreEqual(Colors.White, result);
}
Using Setup/TearDown
14. [TestInitialize]
public async Task InitializeViewModel() {
var user1 = new User {ImageUrl = "http://dummy.jpg", Reputation = 10};
var user2 = new User {ImageUrl = "http://dummy.jpg", Reputation = 10};
var fakeUserRepository = new FakeUserRepository(new[] { user1, user2 });
_viewModel = await InvokeAsync(() => new UserDetailsViewModel(fakeUserRepository));
}
[TestMethod]
public async Task LoadUser_ReputationStaysTheSame_ReputationTrendSame() {
await _viewModel.LoadUser();
await _viewModel.LoadUser();
var result = await InvokeAsync(() => ((SolidColorBrush)_viewModel.ReputationTrend).Color);
Assert.AreEqual(Colors.White, result);
}
private UserDetailsViewModel _viewModel;
16. Extract to methods
[TestMethod]
public async Task LoadUser_ReputationStaysTheSame_ReputationTrendSame() {
var user1 = CreateUser(10);
var user2 = CreateUser(10);
var fakeUserRepository = new FakeUserRepository(new[] { user1, user2 });
var viewModel = await InvokeAsync(() => new UserDetailsViewModel(fakeUserRepository));
await viewModel.LoadUser();
await viewModel.LoadUser();
var result = await InvokeAsync(() => ((SolidColorBrush)viewModel.ReputationTrend).Color);
Assert.AreEqual(Colors.White, result);
}
17. Over abstraction
[TestMethod]
public async Task LoadUser_ReputationStaysTheSame_ReputationTrendSame() {
var fakeUserRepository = new FakeUserRepository(new[] { user1, user2 });
var viewModel = InitializeSystem(10, 10);
await RunTest(2);
CheckColor(Colors.White)
}
18. The problem with Factories
private User CreateUser(int reputation) {
return new User
{
ImageUrl = "http://dummy.jpg",
Reputation = reputation
};
}
private User CreateUser(int reputation, string imageUrl) {
return new User
{
ImageUrl = imageUrl,
Reputation = reputation
};
}
19. Solution: Builder Pattern
class UserBuilder {
private int _id, _reputation;
private string _name, _imageUrl;
public UserBuilder() {
_id = 1;
_name = "dummy";
_imageUrl = "http://dummy.jpg";
}
User Build() {
return new User {
Id = _id, Name = _name, ImageUrl = _imageUrl, Reputation = _reputation
};
}
}
20. Builder Pattern (cont)
class UserBuilder {
...
public UserBuilder WithName(string name) {
_name = name
return this;
}
public UserBuilder WithReputation(int reputation) {
_reputation = reputation
return this;
}
...
}
var user1 = new UserBuilder()
.WithReputation(10)
.Build();
21. Tool: AutoMocking Containers
Test Container SUT
http://blog.ploeh.dk/2013/03/11/auto-mocking-containe
New()
Configure
CreateCreate SUT
Act
23. Can you spot the problem?
[TestMethod]
public void PerformSomeActionReturns42()
{
var myClass = ...
bool initOk = myClass.Initialize();
var result = myClass.PerformSomeAction();
Assert.IsTrue(initOk);
Assert.AreEqual(42, result);
}
24. How about now?
[TestMethod]
public void TestPasswordComplexity()
{
var result = _UserManager.ChangePasswordAsync(_TestUser.Id, "Password123!", "1!").Result;
Assert.IsFalse(result.Succeeded);
result = _UserManager.ChangePasswordAsync(_TestUser.Id, "Password123!", "123456789").Result;
Assert.IsFalse(result.Succeeded);
result = _UserManager.ChangePasswordAsync(_TestUser.Id, "Password123!", "123456789!").Result;
Assert.IsFalse(result.Succeeded);
result = _UserManager.ChangePasswordAsync(_TestUser.Id, "Password123!", "abcdefghijk").Result;
Assert.IsFalse(result.Succeeded);
result = _UserManager.ChangePasswordAsync(_TestUser.Id, "Password123!", "abcdefghijK1!").Result;
Assert.IsTrue(result.Succeeded);
}
http://stackoverflow.com/q/26400537/11361
25. How many Asserts per test?
One Assert per test
2 Asserts == 2 Tests
Really???
26. Multiple Asserts can make sense
[TestMethod]
public void CompareTwoAsserts()
{
var actual = GetNextMessage();
Assert.AreEqual(1, actual.Id);
Assert.AreEqual("str-1", actual.Content);
}
27. HOW UNIT TESTING FRAMEWORKS HURT YOUR UNIT TESTING EFFORTS
WHY TESTS THROW EXCEPTIONS ON FAILURE?
It’s trivial, and best practice
Helps Decouple test runner from test
But not necessary…
28. public class AssertAll
{
public static void Execute(params Action[] assertionsToRun)
{
var errorMessages = new List<exception>();
foreach (var action in assertionsToRun)
{
try
{
action.Invoke();
}
catch (Exception exc)
{
errorMessages.Add(exc);
}
}
if(errorMessages.Any())
{
string errorMessage = string.Join("n", errorMessages);
Assert.Fail($"The following conditions failed:n{errorMessage}"));
}
}
}
http://blog.drorhelper.com/2011/02/multiple-asserts-done-right.html
29. Using Assert.All
[TestMethod]
public void CompareTwoAsserts()
{
var actual = CreateMessage();
AssertAll.Execute(
() => Assert.AreEqual(1, actual.Id),
() => Assert.AreEqual("str-1", actual.Content);
}
30. Some frameworks are catching up!
https://github.com/nunit/docs/wiki/Multiple-Asserts
31. @Test
void dependentAssertions() {
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n")));
},
() -> {
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e")));
});
}
34. How to choose the right Assert?
• IsTrue vs. AreEqual
• Parameter ordering confusion
• StringAssert/CollectionAssert
It’s all about proper error messages
35. AssertHelper
[Test]
public void CompareTest()
{
var myClass = new MyClass();
Expect.That(() => myClass.ReturnFive() == 10);
}
[Test]
public void CompareTest()
{
var c1 = new[] { 1, 2, 3, 4, 5 }
Expect.That(() => c1.Contains(42);
}
https://github.com/dhelper/AssertHelper
36. Assertion Libraries: FluentAssertions
[Fact]
public void CompareTwoObjects()
{
var customer1 = new Customer("cust-1", "John Doe");
var customer2 = new Customer("cust-2", "John Doe");
customer1.ShouldBeEquivalentTo(customer2,
o => o.Excluding(customer => customer.Id));
}
http://www.fluentassertions.com/
38. Test structure issues
• What to call the test?
• AAA is not mandatory
• What should I test?
• How to avoid unreadable, complicated tests?
- Unit testing framework provide no structure
40. Specifications == focused test
Feature: Addition
In order to avoid silly mistakes
As a math idiot
I want to be told the sum of two numbers
Scenario: Add two numbers
Given I have entered 50 into the calculator
And I have entered 70 into the calculator
When I press add
Then the result should be 120 on the screen
45. The right tools will help you write good tests
Arrange
Builder
Pattern
AutoMocking
Containers
Assert
Multiple
Asserts
3rd party
Assertion
libraries
Test
Structure
“Classic” BDD
In-Code BDD
Continuous
Testing
Typemock Runner
DotCover
nCrunch
Live Unit Testing
Infinitest (Java)
Corvlet (.NET Core)
Let’s talk about why you should care about tools.
The reason I want to talk to you about unit testing tools is that In the beginning of my career I have failed miserably with unit tests, , more than once.
Each time I was sure I’ve finally got it and each time I’ve ended up deleting all of my tests.
Today I know that the reason I’ve failed was not because I was a bad developer but because I lacked proper guidance.
Luckily for me I had the pleasure to work with several individuals who taught me how to write good unit tests but there is no reason you should need tt.
Unit testing is not a rocket science all you need is someone to guide you in the right direction. and with the right tools will provide guidance and help you write good unit tests.
SHOW DEMO!
- Two asserts – no idea what caused the failure
Test is testing several things
Left or right? InstanceOf MSTest vs. NUnit