Unit Testing Best Practices
Tomaš 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 #EnjoyIT
Contact me, if you have questions:
• t.maconko@ba.lt
• www.ba.lt

Unit Testing Best Practices

  • 1.
    Unit Testing BestPractices Tomaš Maconko
  • 2.
    What do weknow about UT? • What is it? • Why do we need it? • Who use it? • Who knows how to use it?
  • 3.
    Poor statistics • Whatis the good average code-coverage? • 70-80% • What is the actual average code-coverage? • 10%
  • 4.
    Unit Tests • Smallchunk of code • Automatized test • Determines if specific module is fit for use • Run fast http://martinfowler.com/bliki/images/unitTest
  • 5.
    Purpose • Finds problemsearly • Facilitates change • Documentation • Simplifies integration • Design
  • 6.
    Common issues • Overdesign •Run slowly • Hard to understand • Tests the same thing repeatedly
  • 7.
  • 8.
    #1: Issue • Hugeprojects • Many references • Hundreds of files • Thousands of tests • Hard to focus • Takes time to build the whole solution
  • 9.
    #1: Solution • Groupup the files by certain criteria • Put each tests projects beside logic project • Consider hard cohesion criteria
  • 10.
    #1: Benefits • Smalltests projects • Quick builds and tests runs • Easier to understand
  • 11.
    #2: Problem Seems tobe not so bad?
  • 12.
    #2: Problem • Non-commonclasses 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
  • 13.
    #2: Solution • Removethe 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
  • 14.
    #2: Benefits • Lessprojects 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
  • 15.
    #3: Issue If thetests 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..?
  • 16.
    #3: Issue Can youunderstand it?
  • 17.
    #3: Issue • Classescontain “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
  • 18.
    #3: Solution • Writewhat 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
  • 19.
    #3: Benefits • Clearconditions and results • Better understanding of test
  • 20.
  • 21.
  • 22.
    #4: Issue • Mocksadd 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…
  • 23.
    #4: Issue • WTFexceptions 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)
  • 24.
    #4: Issue • Donot 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.");
  • 25.
    #4: Solution • Donot 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
  • 26.
  • 27.
    #4: Benefits • Lessredundant code • Code is more clear • Asserts are more readable
  • 28.
    #5: Problem Cool littletest isn’t it? Lets look inside…
  • 29.
  • 30.
    #5: Solution • Donot overcommit to reduce the code • Keep code clean, but clear • Remember, other people can have to debug or refactor you code
  • 31.
    #6: Problem // InBusinessLogic: 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
  • 32.
    #6: Solution • Donot 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
  • 33.
    Conclusion • Group testsdepending 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
  • 34.
    Keep #EnjoyIT Contact me,if you have questions: • t.maconko@ba.lt • www.ba.lt