advance unittest
TRANSCRIPT
Advance Unit Testing
Or How I Learned to Stop Worrying and Love the unit-test
Why?
▪Unit-test is the first client of your code
▪Verification and regression through automation
▪Powerful design tool
▪Higher quality code
▪Any many more…
2
Test Doubles
▪Dummies
▪Fakes
▪Mock
▪Stubs
Different types of mocks
▪Pure mock vs nice mock
▪Partial mocks and spies
3
Different type of tests
State Verification
▪Execute one or many methods of objects then assert expected results.
▪They keep continuing to pass even if the internal of the method changes.
Behavior Verification
▪How the system behaves as it operates rather than specifying the expected end state.
▪ It verifies interaction between the objects.
4
Examplepublic class InvitationService {
private final EmailService emailService; private final EmailFilterService redList; public InvitationService(EmailService emailService, EmailFilterService redList) { this.emailService = emailService; this.redList = redList; } public void sendInvitation(Invitation invitation) throws InviteeIsRedListed { if (redList.redlisted(invitation.emailAddress)) { throw new InviteeIsRedListed(); } emailService.sendEmailInvite(invitation); invitation.setIsSent(true); }
}
5
State Verification Sample@Testpublic void testSendInvitation() throws InviteesRedListed { InvitationService sut = new InvitationService(emailService, emailFilterService); Invitation invitation = new Invitation(); invitation.emailAddress = "[email protected]"; sut.sendInvitation(invitation); Assert.assertTrue(invitation.isSent());}@Test(expected = InviteesRedListed.class)public void testSendInvitationGettingFiltered() throws InviteesRedListed { InvitationService sut = new InvitationService(emailService, emailFilterService); Invitation invitation = new Invitation(); invitation.emailAddress = "[email protected]"; sut.sendInvitation(invitation);} 6
Behavior Verification Sample
@Testpublic void testSendInvitation() throws InviteesRedListed { EmailService emailServiceMock = mock(EmailService.class); EmailFilterService filterServiceMock = mock(EmailFilterService.class); InvitationService sut = new InvitationService(emailServiceMock, filterServiceMock); Invitation invitation = new Invitation(); invitation.emailAddress = "[email protected]"; sut.sendInvitation(invitation);// this is not important, if it is set by mock method then we can skip it. Assert.assertTrue(invitation.isSent()); InOrder inOrder = inOrder(emailServiceMock, filterServiceMock); inOrder.verify(filterServiceMock, times(1)).redlisted("[email protected]"); inOrder.verify(emailServiceMock, times(1)).sendEmailInvite(invitation);}
7
Common fallacies of writing testable code
▪Object construction in application logic▪Looking for things instead of asking for
them▪Doing too much work in the constructor▪Keeping global state▪Singleton pattern▪Static methods and utility classes
8
Example
public class ProfileService { ProfileDataLayer dataLayer = new ProfileDataLayer(); ProfileValidator validator; public ProfileService () { validator = ProfileValidator.getValidator(); } public CreateStatus create(String name, String imageUrl) { try { Profile profile = ProfileTransformer.transform(name, imageUrl); validator.validate(profile); dataLayer.insert(profile); return CreateStatus.SUCCEED; } catch (Exception e) { return CreateStatus.FAILED; } }
Object construction
Looking for object
Static method call
9
Fixed example
public class ProfileService {
final ProfileDataLayer dataLayer; final ProfileValidator validator; final ProfileTransformer transformer
@Inject public ProfileService ( ProfileDataLayer dataLayer, ProfileValidator validator, ProfileTransformer transformer) { this.validator = validator; this.dataLayer = dataLayer; this.transformer = transformer; }
Objects passed in the constructor through dependency injection framework
10
Fixed example
public CreateStatus create(String name, String imageUrl) { try { Profile profile = transformer.transform(name, imageUrl); validator.validate(profile); dataLayer.insert(profile); return CreateStatus.SUCCEED; } catch (Exception e) { return CreateStatus.FAILED; } }
No knowledge and dependency on implementation and where logic comes from.
11
Common fallacies of writing testable code
▪Overusing inheritance as a method of code reuse
▪Lots of conditionals (IF and SWITCH statements)
▪Mixing value objects and service objects▪Breaking SRP (Single Responsibility
Principle)
12
Solutions overview▪ Use dependency injection frameworks
▪ Favor composition over inheritance
▪ Favor polymorphism over conditions▪ Command pattern, strategy patter, chain of
responsibility, etc▪ Respect SRP
▪ RedFlag#1: Classes with bad postfix (Util, Helper, Manager)
▪ RedFlag#2: Classes with more than 250 lines of codes.
▪ RedFlag#3: Classes that have “And” or “OR” in their names.
▪ RedFlag#4: Low Cohesion▪ Validators, Transformation, Formatting, etc
should be in its own class.
13
Ultimate solution
▪Test Driven Development - TDD
▪Always prefer TDD for the system that you own and you understanding the domain of the problem very well.
▪Test-after is OK when you are maintaining a system or working on a horizontal feature that you will need to change lots of other people code.
14
Testing Legacy Code
▪In this context Legacy code is a code that is written and designed without having testing in mind, and it is still live code.
▪They are usually hard to test.
15
Testing Legacy Code
▪Refactoring is a powerful tool▪Introduce wrappers and
adapters▪Change access modifiers on
methods to make them visible for testing ▪public @interface VisibleForTesting {}
16
Writing Clean Test
17
Coding Style
Production
▪DRY (Don’t Repeat Yourself)
▪High cohesion. ▪Methods are interacting with
each other
▪Lifecycle at class level
Unit Test
▪DAMP (Descriptive And Meaningful Phrases)
▪Low cohesion▪Each test (method) are self-
contained
▪Lifecycle at test methods.
▪Meaningful and long method names.
18
DAMP vs DRY
DRY example:
public void test1() {
setupTestFixture(ps1, ps2, ps3);
setupMocks(pm1, pm2);
verify(pv1, pv2, pv3);
}
▪Not readable in its entirety.
19
Clean tests
▪Avoid testing several things in one test▪When you need to persist data for your test,
persist in-memory▪Setup and cleanup your test▪Test must run fast. Really fast!
▪No sleeping thread
20
Clean tests (part 2)
▪Make your test have a Cyclomatic Complexity of one. Tests should not have indention or indirections.
▪Beware of Liar anti-pattern, always observe test failures
▪Do not use try-catch blocks in your tests▪Do not be a mock-happy tester▪Do not target high code coverage
21
Test life cycle, threading and frameworks
▪ Tests’ life cycle is separate from applications
▪ For example TestNG runs Suite, Group, Class, Test, Method configurations before and after tests
▪ Tests have different threading models
▪ Parallel forks vs single threaded
▪ Frameworks do a lot of legwork
▪ Exception and failure handling
▪ Reports
▪ Tooling: Assertions, data providers, factories, dependencies
22
Be mindful of scopes
@Mock Map<String, Integer> counters;
@BeforeTestpublic void setUp() { MockitoAnnotations.initMocks(this); }
@Test public void testCount1() { Mockito.when(counters.get(TEST_KEY)).thenReturn(1); Assert.assertEquals((int) counters.get(TEST_KEY), 1); Mockito.verify(counters).get(TEST_KEY);}
@Test public void testCount2() { Mockito.when(counters.get(TEST_KEY)).thenReturn(2); Assert.assertEquals((int) counters.get(TEST_KEY), 2); Mockito.verify(counters).get(TEST_KEY);}
Verification fails since mock is set up for the whole test.(use e.g. BeforeMethod)
23
Unit Test Design Patterns
24
Can we write a test that is both DAMP and DRY?
The answer is Yes!
25
Test Data Builder
public class UserProfileInputBuilder { private String firstName; private String lastName;
public UserProfileInputBuilder() { this.firstName = "ftest"; this.lastName = "ltest"; ... }
public UserProfileInputBuilder withFirstName(String firstName) { this.firstName = firstName; return this; }
public UserProfileInputBuilder withLastName(String lastName) { this.lastName = lastName; return this; }
...
public UserProfile build() { return new UserProfile(this.firstName, this.lastName); }}
26
How to use it
EmailValidator emailValidatorMock = Mockito.mock(EmailValidator.class); LocalizeName localizerMock = Mockito.mock(LocalizeName.class);
ProfileService sut = new ProfileService(emailValidatorMock, localizerMock);
UserProfile invalidInput = new UserProfileInputBuilder() .withEmail("invalid email format") .build();
Mockito.when(emailValidator.validate(invalidInput.getEmail()).thenReturn(false));
//Exercise system and expect InvalidInputException//Note that create user is suppose to throw an exception when the email is invalid//We do not need to provide noise and create locale, firstName, and lastName//for this test case. sut.createUser(invalidInput);
27
Test Data Providers
@DataProvider(name = "drinks")public Object[][] getDrinks() { return new Object[][] { { "Whiskey", true }, { "Daiquiri", false } };}
@Test(dataProvider = "drinks")public void testDrinkingService( String drink, boolean shouldPass) { Assert.assertEquals(service.isGood(drink), shouldPass);}
28
Q&AHope you enjoyed!
29