34
Unit Testing Best Practices Tomaš Maconko

Unit Testing Best Practices

Embed Size (px)

Citation preview

Unit Testing Best PracticesTomaš Maconko

What do we know about UT?

• What is it?

• Why do we need it?

• Who use it?

• Who knows how to use it?

Poor statistics

• What is the good average code-coverage?

• 70-80%

• What is the actual average code-coverage?

• 10%

Unit Tests

• Small chunk of code

• Automatized test

• Determines if specific module is fit for use

• Run fast

http://martinfowler.com/bliki/images/unitTest

Purpose

• Finds problems early

• Facilitates change

• Documentation

• Simplifies integration

• Design

Common issues

• Overdesign

• Run slowly

• Hard to understand

• Tests the same thing repeatedly

#1: Issue

#1: Issue

• Huge projects

• Many references

• Hundreds of files

• Thousands of tests

• Hard to focus

• Takes time to build the whole solution

#1: Solution

• Group up the files by certain criteria

• Put each tests projects beside logic project

• Consider hard cohesion criteria

#1: Benefits

• Small tests projects

• Quick builds and tests runs

• Easier to understand

#2: Problem

Seems to be not so bad?

#2: Problem

• Non-common classes in common unit tests project

• Redundant dependencies through common unit tests project

• It references several projects, that also reference several projects, that ... So after every change on referenced project we have to build the whole chain.

• It takes time to build

#2: Solution

• Remove the code from common unit tests project that is not-common and put it to each tests project that uses that code

• Consider some code duplication – you can always copy the certain code to each project – you don’t always have to be DRY

• Remove common unit tests project to remove the additional reference build chain

#2: Benefits

• Less projects to build = less time to build

• Utilities are in the same project with tests that use it

• Changes used for specific tests projects utility does not affect other projects

#3: Issue

If the tests passed, then it would be ok. But what if it fails? Do you

understand what the test should do? Or you want to go to look at code..?

#3: Issue

Can you understand it?

#3: Issue

• Classes contain “Test” postfix when there are many tests in class

• Not always obvious what are the conditions and expected results

• Soooo hard to analyze the problem if the test is reeaaally big

#3: Solution

• Write what you test or why you test

• Use naming conventions for test methods:

• [Class]_[Method]_[Conditions]_[ExpectedResults]

• [Api]_[Conditions]_[ExpectedResults]

• Use naming conventions for test classes:

• [Testable]Tests

• In some cases write comments

• Add simple comments “Arrange, Act, Assert” to outline the test sections

#3: Benefits

• Clear conditions and results

• Better understanding of test

#4: Issue

SETUP

#4: Issue

#4: Issue

• Mocks add a lot of confusion to code

• Many lines of useless and repeated code

• You have to put effort to mock the data in a right way

• And…

#4: Issue

• WTF exceptions

Moq.MockException: Wrong external request.

Expected invocation on the mock once, but was 0 times: r => r.AddToLog(It.Is<DefaultLogData>(d =>

(((((((((((d.CorrelationId == String.Empty && d.Identifier == String.Empty) && d.OperationId == Guid.Empty) && d.Method

== ._method) && (Int32)d.ObjectType == 0) && d.StatusCode == "0") && d.StatusCodeDescription == String.Empty) &&

d.SubStatusCode == null) && d.SubStatusCodeDescription == null) && d.ExternalType ==

.ExternalRequest.GetType().FullName) && d.Milliseconds == 0) && .ContainsAllStrings(d.SerializedExternalObject, new[] {

.ExternalRequest.SampleData })) && d.Url == "http://dummyservice.payex.com/"))

Configured setups:

r => r.AddToLog(It.IsAny<DefaultLogData>()), Times.Exactly(4)

Performed invocations:

IDefaultLogRepository.AddToLog(Infrastructure.MPS.Common.Logging.TransportClasses.DefaultLogData)

IDefaultLogRepository.AddToLog(Infrastructure.MPS.Common.Logging.TransportClasses.DefaultLogData)

IDefaultLogRepository.AddToLog(Infrastructure.MPS.Common.Logging.TransportClasses.DefaultLogData)

IDefaultLogRepository.AddToLog(Infrastructure.MPS.Common.Logging.TransportClasses.DefaultLogData)

#4: Issue

• Do not combine Times.Never with argument matching – tests can pass the

verification by because of value mismatch

_logRepository.Verify(r => r.LogRequest(It.Is<FinancialRequestLogData>(

l => l.CurrentTransactionNumber == 0 &&

l.OriginalTransactionNumber == 0 &&

l.WorkflowExtension == string.Empty && l.OrderId == null &&

l.OrderRef == null && l.Operation == default(FinancialOperation) &&

string.IsNullOrWhiteSpace(l.SerializedExternal) &&

!string.IsNullOrEmpty(l.SerializedInternal) && l.Url == Url)),

Times.Never, "Wrong request.");

#4: Solution

• Do not use entity mocks – write you own overridden class with predefined data

• You can always use builder pattern (object mother pattern) or fixture to build needed class for testing, either using mocking framework or overridden classes

• If you still want to use mocking frameworks, prefer the explicit assert instead of complicated verification

#4: Solution

#4: Benefits

• Less redundant code

• Code is more clear

• Asserts are more readable

#5: Problem

Cool little test isn’t it? Lets look inside…

#5: Problem

#5: Solution

• Do not overcommit to reduce the code

• Keep code clean, but clear

• Remember, other people can have to debug or refactor you code

#6: Problem

// In BusinessLogic:

var hasher = new MyHasher();

var hashBuilder = new StringBuilder();

hashBuilder.Append(clientId);

hashBuilder.Append(clientFirstName);

hashBuilder.Append(secretKey);

var hash = hasher.GetHash(hashBuilder.ToString());

// In UnitTests

var hasher = new SimilarMyHasher();

string hash = clientId + secretKey;

var hash = hasher.GetUtHash(hash);

• Duplicated logic creates tests instability

#6: Solution

• Do not test the logic that is tested in another test

• Reuse the logic for specific cases from real business logic

• Duplicate the stuff only if you want to check if method returns the right data

• Last example could be useful when checking if business logic hash calculation is correct

Conclusion

• Group tests depending on their cohesion – integration, API, requirement, ...

• Create self-documenting tests

• Do not test several things in the same test

• If you feel your test will create many WTFs/min – refactor it! – REFACTOR TILL YOU DROP

Keep #EnjoyITContact me, if you have questions:

[email protected]

• www.ba.lt