MADRID · NOV 18-19 · 2016
Why Using
by @andres_viedma
instead of
in your Java Tests
Andrés ViedmaAndrés Viedma
@andres_viedma@andres_viedma
01 SPOCK?
A testing framework
Tests are written in
100% compatible with Java code
Runs on
http://spockframework.org/
A basic JUnit test
@Test
public void thatAddAPositiveNumberToZeroReturnsTheSamePositiveNumber()
throws Exception {
int positiveNumber = 23;
int zero = 0;
int result = zero + positiveNumber;
assertEquals(positiveNumber, result);
}
A basic Spock test
def "Add a positive number to zero returns the same positive number"() {
given:
def positiveNumber = 23
def zero = 0
when:
def result = zero + positiveNumber
then:
result == positiveNumber
}
A basic Spock test
def "Add a positive number to zero returns the same positive number"() {
given:
def positiveNumber = 23
def zero = 0
when:
def result = zero + positiveNumber
then:
result == positiveNumber
}
A basic Spock test
DSL
def "Add a positive number to zero returns the same positive number"() {
given:
def positiveNumber = 23
def zero = 0
when:
def result = zero + positiveNumber
then:
result == positiveNumber
}
String literals
def "Add a positive number to zero returns the same positive number"() {
(...)
}
@Test
public void thatAddAPositiveNumberToZeroReturnsTheSamePositiveNumber()
throws Exception {
(...)
}
String literals
def "Add a positive number to zero returns the same positive number"() {
(...)
}
@Test
public void thatAddAPositiveNumberToZeroReturnsTheSamePositiveNumber()
throws Exception {
(...)
}
def "En un lugar de la Mancha, de cuyo nombre no quiero acordarme,
no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero,
adarga antigua, rocín flaco y galgo corredor. "() {
(...)
}
Spock Blocks
given:
def positiveNumber = 23
def zero = 0
when:
def result = zero + positiveNumber
then:
result == positiveNumber
int positiveNumber = 23;
int zero = 0;
int result = zero + positiveNumber;
assertEquals(positiveNumber, result);
Programming vs. Specification
Spock Blocks
given:
def positiveNumber = 23
def zero = 0
when:
def result = zero + positiveNumber
then:
result == positiveNumber
// Given
int positiveNumber = 23;
int zero = 0;
// When
int result = zero + positiveNumber;
// Then
assertEquals(positiveNumber, result);
Compilation check
Programming vs. Specification
Documented Spock Blocks
given: "A positive number"
def positiveNumber = 23
and: "Well... a SPECTACULAR zero"
def zero = 0
when: "The number is added to zero"
def result = zero + positiveNumber
then: "The result is the same number"
result == positiveNumber
int positiveNumber = 23;
int zero = 0;
int result = zero + positiveNumber;
assertEquals(positiveNumber, result);
Documents purpose
Helps thinking
expect block
given: "A positive number"
def positiveNumber = 23
expect: "That added to zero results the same number"
zero + positiveNumber == positiveNumber
Replaces when-then for simple
functional tests
Expectations (then / expect)
given:
def positiveNumber = 23
def zero = 0
when:
def result = zero + positiveNumber
then:
result == positiveNumber
int positiveNumber = 23;
int zero = 0;
int result = zero + positiveNumber;
assertEquals(positiveNumber, result);
Multiple expectations
then:
result == positiveNumber
!collection.isEmpty()
then:
result == positiveNumber
and:
!collection.isEmpty()
Multiple expectations
then:
result == positiveNumber
!collection.isEmpty()
then:
def expectedResult = positiveNumber
result == expectedResult
Only conditions and variable
assignments allowed
then:
result == positiveNumber
and:
!collection.isEmpty()
def "Crash if zero"() {
when:
object.crashIfZero(0)
then:
thrown(ZeroException)
}
@Test(expected = OperationException.class)
public void crashIfZero()
throws Exception {
object.crashIfZero(0);
}
Exception conditions
def "Crash if zero"() {
when:
object.crashIfZero(0)
then:
thrown(ZeroException)
}
@Test(expected = OperationException.class)
public void crashIfZero()
throws Exception {
object.crashIfZero(0);
}
def "Dont crash if not zero"() {
when:
object.crashIfZero(1)
then:
notThrown(ZeroException)
}
@Test // No exception should be thrown
public void dontCrashIfNotZero()
throws Exception {
object.crashIfZero(1);
}
Exception conditions
class KakaSpec extends Specification {
static final SUBSCRIPTION_ID = 27
private AuxiliarObject auxiliar
void setup() {
(...)
}
def "xxx"() {
(...)
}
}
public class XxxxxxxxxxTest {
private static final long SUBSCRIPTION_ID = 27;
private AuxiliarObject auxiliar;
@Before
public void setUp() throws Exception {
(...)
}
@Test
public void thatXxx() throws Exception {
(...)
}
}
The test class
Groovy syntax sugar
when:
def result = object.getRecords()
then:
result?.list == [1, 47, 23]
Optional types
Collection literals
No ; needed
== operator for equals
Safe navigation ?. operator
Improved error output
Condition not satisfied:
result.value == expectedResult
| | | |
| | | 12
| | false
| 10
PaymentResult(id=2,value=10)
02 MOCKING
Mocks Creation
@Mock
private CustomerDataRegistry customerRegistry;
(...)
private PaymentsCoordinator paymentCoordinator;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
paymentCoordinator = new PaymentsCoordinator(
customerRegistry, (...));
}
CustomerDataRegistry customerRegistry = Mock()
(...)
@Subject PaymentsCoordinator paymentCoordinator =
new PaymentsCoordinator(customerRegistry, (...))
Mocks Creation
@Mock
private CustomerDataRegistry customerRegistry;
(...)
private PaymentsCoordinator paymentCoordinator;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
paymentCoordinator = new PaymentsCoordinator(
customerRegistry, (...));
}
CustomerDataRegistry customerRegistry = Mock()
(...)
@Subject PaymentsCoordinator paymentCoordinator =
new PaymentsCoordinator(customerRegistry, (...))
Mocks Creation
@Mock
private CustomerDataRegistry customerRegistry;
(...)
private PaymentsCoordinator paymentCoordinator;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
paymentCoordinator = new PaymentsCoordinator(
customerRegistry, (...));
}
CustomerDataRegistry customerRegistry = Mock()
(...)
@Subject PaymentsCoordinator paymentCoordinator =
new PaymentsCoordinator(customerRegistry, (...))
Responses declaration
@Test
public void testSuccessfulPaymentWithNewCreditCard() throws Exception {
when(customerRegistry.getCustomerData(SUBSCRIPTION_ID)).thenReturn(CUSTOMER_DATA);
(...)
PaymentResult result = paymentCoordinator.doPayment(paymentData);
(...)
}
def "Succesful payment with new credit card"() {
given: "A registered customer"
customerRegistry.getCustomerData(SUBSCRIPTION_ID) >> CUSTOMER_DATA
(...)
when: "A payment is requested"
def result = paymentCoordinator.doPayment(paymentData)
(...)
}
Interaction expectations
@Test
public void testSuccessfulPaymentWithNewCreditCard() throws Exception {
(...)
when(paymentInteractor.performPaymentInProvider(inputFields))
.thenReturn(PAYMENT_SUCCESSFUL_OUTPUT);
PaymentResult result = paymentCoordinator.doPayment(paymentData);
verify(paymentInteractor).performPaymentInProvider(inputFields)).
(...)
}
def "Succesful payment with new credit card"() {
(...)
when: "A payment is requested"
def result = paymentCoordinator.doPayment(paymentData)
then: "It is sent to the payment provider with successful result"
1 * paymentInteractor.performPaymentInProvider(inputFields) >>
PAYMENT_SUCCESSFUL_OUTPUT
(...)
}
Mocks and Stubs
then:
0 * _
Semantic
Lenient: default
values
Only return values
Stubs: empty objects
Stubs: no interaction
expectations
Mocks: nulls
Call Matching
Arguments matching
mock.method("hello")
mock.method(!"hello")
mock.method()
mock.method(_)
mock.method(*_)
mock.method(_ as String)
mock.method({ l -> l.size() > 3 })
Method matching
customerRegistry._
_
customerRegistry./searchBy.*/(…)
Interactions expectations matching
Parametes matching
Order
1 * subscriber.receive("hello")
0 * subscriber.receive("hello")
(1..3) * subscriber.receive("hello")
(1.._) * subscriber.receive("hello")
(_..3) * subscriber.receive("hello")
_ * subscriber.receive("hello")
then:
1 * mock1.method1(...)
and:
1 * mock2.method2(...)
then:
1 * mock3.calledAfter1And2()
Cardinality * matching constraints
Forget the matchers
@Test
public void testSuccessfulPaymentWithNewCreditCard() throws Exception {
(...)
ArgumentMatcher<Payment> paymentMatcher = new ArgumentMatcher<Payment>() {
public boolean matches(Object payment) {
return ((Payment) payment).getPaymentType() != null;
}
};
verify(paymentInteractor, never()).performPaymentInProvider(
paymentMatcher, anyObject(), eq(VALIDATE));
(...)
}
def "Succesful payment with new credit card"() {
(...)
then:
0 * paymentInteractor.performPaymentInProvider(
{ payment -> payment.storedCard != null }, _, VALIDATE)
(...)
}
03 SOME
PRACTICAL USES
Property-based testing (JUnit parameterized tests)
@RunWith(Parameterized.class)
public class PaymentProcessingOfANewCompletedResponseAuthorizationHandlerTest {
public static Object[][] data() {
return new Object[][] {
{ TransactionStatus.INITIALIZED, TransactionStatus.AUTHORIZED },
{ TransactionStatus.INITIALIZED, TransactionStatus.REFUSED },
{ TransactionStatus.ACCEPTED, TransactionStatus.AUTHORIZED },
{ TransactionStatus.ACCEPTED, TransactionStatus.REFUSED }
};
}
@Parameter public TransactionStatus currentStatus;
@Parameter(value=1) public TransactionStatus responseStatus;
@Test
public void test() throws Exception {
(...)
}
}
Property-based testing (JUnit parameterized tests)
@RunWith(Parameterized.class)
public class PaymentProcessingOfANewCompletedResponseAuthorizationHandlerTest {
public static Object[][] data() {
return new Object[][] {
{ TransactionStatus.INITIALIZED, TransactionStatus.AUTHORIZED },
{ TransactionStatus.INITIALIZED, TransactionStatus.REFUSED },
{ TransactionStatus.ACCEPTED, TransactionStatus.AUTHORIZED },
{ TransactionStatus.ACCEPTED, TransactionStatus.REFUSED }
};
}
@Parameter public TransactionStatus currentStatus;
@Parameter(value=1) public TransactionStatus responseStatus;
@Test
public void test() throws Exception {
(...)
}
}
Property-based testing (JUnit parameterized tests)
@RunWith(Parameterized.class)
public class PaymentProcessingOfANewCompletedResponseAuthorizationHandlerTest {
public static Object[][] data() {
return new Object[][] {
{ TransactionStatus.INITIALIZED, TransactionStatus.AUTHORIZED },
{ TransactionStatus.INITIALIZED, TransactionStatus.REFUSED },
{ TransactionStatus.ACCEPTED, TransactionStatus.AUTHORIZED },
{ TransactionStatus.ACCEPTED, TransactionStatus.REFUSED }
};
}
@Parameter public TransactionStatus currentStatus;
@Parameter(value=1) public TransactionStatus responseStatus;
@Test
public void test() throws Exception {
(...)
}
}
Property-based testing (JUnit parameterized tests)
@RunWith(Parameterized.class)
public class PaymentProcessingOfANewCompletedResponseAuthorizationHandlerTest {
public static Object[][] data() {
return new Object[][] {
{ TransactionStatus.INITIALIZED, TransactionStatus.AUTHORIZED },
{ TransactionStatus.INITIALIZED, TransactionStatus.REFUSED },
{ TransactionStatus.ACCEPTED, TransactionStatus.AUTHORIZED },
{ TransactionStatus.ACCEPTED, TransactionStatus.REFUSED }
};
}
@Parameter public TransactionStatus currentStatus;
@Parameter(value=1) public TransactionStatus responseStatus;
@Test
public void test() throws Exception {
(...)
}
}
Property-based testing (Spock where)
@Unroll
def "Processing of a new completed response from status #currentStatus to #responseStatus"(
currentStatus, responseStatus) {
(...)
when:
def resultTransaction = authorizationHandler.processResponse(statusInfo, FROM_RETRY)
then:
(...)
where:
currentStatus | responseStatus
TransactionStatus.INITIALIZED | TransactionStatus.AUTHORIZED
TransactionStatus.INITIALIZED | TransactionStatus.REFUSED
TransactionStatus.ACCEPTED | TransactionStatus.AUTHORIZED
TransactionStatus.ACCEPTED | TransactionStatus.REFUSED
}
Property-based testing (Spock where)
@Unroll
def "Processing of a new completed response from status #currentStatus to #responseStatus"(
currentStatus, responseStatus) {
(...)
when:
def resultTransaction = authorizationHandler.processResponse(statusInfo, FROM_RETRY)
then:
(...)
where:
currentStatus | responseStatus
TransactionStatus.INITIALIZED | TransactionStatus.AUTHORIZED
TransactionStatus.INITIALIZED | TransactionStatus.REFUSED
TransactionStatus.ACCEPTED | TransactionStatus.AUTHORIZED
TransactionStatus.ACCEPTED | TransactionStatus.REFUSED
}
Config tests (multiline and String interpolation)
def "getRejectionReasonForError with invalid reason mapped"() {
given:
def yaml = """
paymentForm:
rejectionReasonMapping:
${ERROR_CODE}: '${REJECTION_REASON_INVALID}'
"""
def config = formConfig(yaml)
when:
def returnedReason = config.getRejectionReasonForError(ERROR_CODE)
then: "Returns general error"
returnedReason == PaymentRejectionReason.GENERAL_ERROR
}
Config tests (multiline and String interpolation)
def "getRejectionReasonForError with invalid reason mapped"() {
given:
def yaml = """
paymentForm:
rejectionReasonMapping:
${ERROR_CODE}: '${REJECTION_REASON_INVALID}'
"""
def config = formConfig(yaml)
when:
def returnedReason = config.getRejectionReasonForError(ERROR_CODE)
then: "Returns general error"
returnedReason == PaymentRejectionReason.GENERAL_ERROR
}
Builders?
def payment = new PaymentTransaction(
id: TRANSACTION_ID,
subscriptionId: SUBSCRIPTION_ID,
amount: AMOUNT,
lastChangeTimestamp: OLD_TIMESTAMP)
PaymentTransaction payment = new PaymentTransactionBuilder()
.id(TRANSACTION_ID)
.subscriptionId(SUBSCRIPTION_ID)
.amount(AMOUNT)
.lastChangeTimestamp(OLD_TIMESTAMP)
.build();
Default constructor with
named parameters
Builders?
def payment = new PaymentTransaction(
id: TRANSACTION_ID,
subscriptionId: SUBSCRIPTION_ID,
amount: AMOUNT,
lastChangeTimestamp: OLD_TIMESTAMP)
PaymentTransaction payment = new PaymentTransactionBuilder()
.id(TRANSACTION_ID)
.subscriptionId(SUBSCRIPTION_ID)
.amount(AMOUNT)
.lastChangeTimestamp(OLD_TIMESTAMP)
.build();
Default constructor with
named parameters
def "Process an authorized payment "() {
given: "An already authorized payment"
def authorizedPayment = paymentTransactionWith(status: TransactionStatus.AUTHORIZED)
(...)
}
private PaymentTransaction paymentTransactionWith(Map overrides) {
def attrs = [
id: TRANSACTION_ID,
subscriptionId: SUBSCRIPTION_ID,
amount: AMOUNT,
status: TransactionStatus.INITIALIZED,
lastChangeTimestamp: OLD_TIMESTAMP
] + overrides
return new PaymentTransaction(attrs)
}
Objects with default properties
def "Process an authorized payment "() {
given: "An already authorized payment"
def authorizedPayment = paymentTransactionWith(status: TransactionStatus.AUTHORIZED)
(...)
}
private PaymentTransaction paymentTransactionWith(Map overrides) {
def attrs = [
id: TRANSACTION_ID,
subscriptionId: SUBSCRIPTION_ID,
amount: AMOUNT,
status: TransactionStatus.INITIALIZED,
lastChangeTimestamp: OLD_TIMESTAMP
] + overrides
return new PaymentTransaction(attrs)
}
Objects with default properties
Private methods for readability?
PaymentTransaction transaction = givenAnAuthorizedTransaction();
paymentProcessor.processPayment(transaction, RESPONSE_DATA);
verifyTheTransactionIsTransitionedTo(ADDING_BALANCE);
verifyTheTopupIsProcessedCorrectly();
Private methods for readability?
given: "An already authorized transaction"
def transaction = transactionWith(status: AUTHORIZED)
transactionRegistry.getTransactionInfo(TRANSACTION_ID) >> transaction
when: "The payment is processed"
paymentProcessor.processPayment(transaction, RESPONSE_DATA)
then: "The transaction is transitioned to adding balance status"
1 * transactionRegistry.changeTransactionStatus(
TRANSACTION_ID, TransactionStatus.ADDING_BALANCE) >>
transactionWith(status: ADDING_BALANCE)
then: "The topup is processed"
1 * topupInteractor.topup(transaction, RESPONSE_DATA) >>
new PaymentStatusInfo(TRANSACTION_ID, TransactionStatus.BALANCE_ADDED)
PaymentTransaction transaction = givenAnAuthorizedTransaction();
paymentProcessor.processPayment(transaction, RESPONSE_DATA);
verifyTheTransactionIsTransitionedTo(ADDING_BALANCE);
verifyTheTopupIsProcessedCorrectly();
static final MERCADOPAGO_RESULT = [
status: 'approved',
status_detail: 'ok',
description: 'Tuenti (test)',
id: 999999,
authorization_code: 858,
collector_id: 5678,
statement_descriptor: 'WWW.MERCADOPAGO.COM',
card: [
last_four_digits: 1234,
expiration_year: 2016,
expiration_month: 12
],
payer: [
first_name: NAME,
last_name: LAST_NAME,
email: EMAIL,
]
]
Tests involving 3rd party APIs – Thank God for Collection
literals
04 So
what...?
●
Increase abstraction level
Not “programming tests” ® specify test cases
Easy + powerful
Expressivity ® test is also documentation
● Easy to run in continuous integration systems / IDEs
● Better error detection info
Advantages?
● Code Refactors not so safe
● Mocks can only be created in the Spec class
Integration tests with dependency injection overrides ... more
difficult, but possible!
Disadvantages?
● Code Refactors not so safe
● Mocks can only be created in the Spec class
Integration tests with dependency injection overrides ... more
difficult, but possible!
Disadvantages?
class BaseIntegrationSpecification extends TIntegrationSpecification {
@InjectOverride MercadopagoClient mercadopago = Mock()
@Inject PaymentNotificationsService paymentNotificationsServiceMock
(...)
@TIntegrationTestsModule
static class MockedBoundariesModule extends SpockMocksModule {
(...)
}
}
● Code Refactors not so safe
● Mocks can only be created in the Spec class
Integration tests with dependency injection overrides ... more
difficult, but possible!
Disadvantages?
class BaseIntegrationSpecification extends TIntegrationSpecification {
@InjectOverride MercadopagoClient mercadopago = Mock()
@Inject PaymentNotificationsService paymentNotificationsServiceMock
(...)
@TIntegrationTestsModule
static class MockedBoundariesModule extends SpockMocksModule {
(...)
}
}
Do you dare to change?
Do you dare to change?
Andrés ViedmaAndrés Viedma
@andres_viedma@andres_viedma
Questions?

Por qué usar Spock en lugar de JUnit / Mockito para tus tests Java - Codemotion 2016

  • 1.
    MADRID · NOV18-19 · 2016 Why Using by @andres_viedma instead of in your Java Tests
  • 2.
  • 3.
  • 4.
    A testing framework Testsare written in 100% compatible with Java code Runs on http://spockframework.org/
  • 5.
    A basic JUnittest @Test public void thatAddAPositiveNumberToZeroReturnsTheSamePositiveNumber() throws Exception { int positiveNumber = 23; int zero = 0; int result = zero + positiveNumber; assertEquals(positiveNumber, result); }
  • 6.
    A basic Spocktest def "Add a positive number to zero returns the same positive number"() { given: def positiveNumber = 23 def zero = 0 when: def result = zero + positiveNumber then: result == positiveNumber }
  • 7.
    A basic Spocktest def "Add a positive number to zero returns the same positive number"() { given: def positiveNumber = 23 def zero = 0 when: def result = zero + positiveNumber then: result == positiveNumber }
  • 8.
    A basic Spocktest DSL def "Add a positive number to zero returns the same positive number"() { given: def positiveNumber = 23 def zero = 0 when: def result = zero + positiveNumber then: result == positiveNumber }
  • 9.
    String literals def "Adda positive number to zero returns the same positive number"() { (...) } @Test public void thatAddAPositiveNumberToZeroReturnsTheSamePositiveNumber() throws Exception { (...) }
  • 10.
    String literals def "Adda positive number to zero returns the same positive number"() { (...) } @Test public void thatAddAPositiveNumberToZeroReturnsTheSamePositiveNumber() throws Exception { (...) } def "En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. "() { (...) }
  • 11.
    Spock Blocks given: def positiveNumber= 23 def zero = 0 when: def result = zero + positiveNumber then: result == positiveNumber int positiveNumber = 23; int zero = 0; int result = zero + positiveNumber; assertEquals(positiveNumber, result); Programming vs. Specification
  • 12.
    Spock Blocks given: def positiveNumber= 23 def zero = 0 when: def result = zero + positiveNumber then: result == positiveNumber // Given int positiveNumber = 23; int zero = 0; // When int result = zero + positiveNumber; // Then assertEquals(positiveNumber, result); Compilation check Programming vs. Specification
  • 13.
    Documented Spock Blocks given:"A positive number" def positiveNumber = 23 and: "Well... a SPECTACULAR zero" def zero = 0 when: "The number is added to zero" def result = zero + positiveNumber then: "The result is the same number" result == positiveNumber int positiveNumber = 23; int zero = 0; int result = zero + positiveNumber; assertEquals(positiveNumber, result); Documents purpose Helps thinking
  • 14.
    expect block given: "Apositive number" def positiveNumber = 23 expect: "That added to zero results the same number" zero + positiveNumber == positiveNumber Replaces when-then for simple functional tests
  • 15.
    Expectations (then /expect) given: def positiveNumber = 23 def zero = 0 when: def result = zero + positiveNumber then: result == positiveNumber int positiveNumber = 23; int zero = 0; int result = zero + positiveNumber; assertEquals(positiveNumber, result);
  • 16.
    Multiple expectations then: result ==positiveNumber !collection.isEmpty() then: result == positiveNumber and: !collection.isEmpty()
  • 17.
    Multiple expectations then: result ==positiveNumber !collection.isEmpty() then: def expectedResult = positiveNumber result == expectedResult Only conditions and variable assignments allowed then: result == positiveNumber and: !collection.isEmpty()
  • 18.
    def "Crash ifzero"() { when: object.crashIfZero(0) then: thrown(ZeroException) } @Test(expected = OperationException.class) public void crashIfZero() throws Exception { object.crashIfZero(0); } Exception conditions
  • 19.
    def "Crash ifzero"() { when: object.crashIfZero(0) then: thrown(ZeroException) } @Test(expected = OperationException.class) public void crashIfZero() throws Exception { object.crashIfZero(0); } def "Dont crash if not zero"() { when: object.crashIfZero(1) then: notThrown(ZeroException) } @Test // No exception should be thrown public void dontCrashIfNotZero() throws Exception { object.crashIfZero(1); } Exception conditions
  • 20.
    class KakaSpec extendsSpecification { static final SUBSCRIPTION_ID = 27 private AuxiliarObject auxiliar void setup() { (...) } def "xxx"() { (...) } } public class XxxxxxxxxxTest { private static final long SUBSCRIPTION_ID = 27; private AuxiliarObject auxiliar; @Before public void setUp() throws Exception { (...) } @Test public void thatXxx() throws Exception { (...) } } The test class
  • 21.
    Groovy syntax sugar when: defresult = object.getRecords() then: result?.list == [1, 47, 23] Optional types Collection literals No ; needed == operator for equals Safe navigation ?. operator
  • 22.
    Improved error output Conditionnot satisfied: result.value == expectedResult | | | | | | | 12 | | false | 10 PaymentResult(id=2,value=10)
  • 23.
  • 24.
    Mocks Creation @Mock private CustomerDataRegistrycustomerRegistry; (...) private PaymentsCoordinator paymentCoordinator; @Before public void setup() { MockitoAnnotations.initMocks(this); paymentCoordinator = new PaymentsCoordinator( customerRegistry, (...)); } CustomerDataRegistry customerRegistry = Mock() (...) @Subject PaymentsCoordinator paymentCoordinator = new PaymentsCoordinator(customerRegistry, (...))
  • 25.
    Mocks Creation @Mock private CustomerDataRegistrycustomerRegistry; (...) private PaymentsCoordinator paymentCoordinator; @Before public void setup() { MockitoAnnotations.initMocks(this); paymentCoordinator = new PaymentsCoordinator( customerRegistry, (...)); } CustomerDataRegistry customerRegistry = Mock() (...) @Subject PaymentsCoordinator paymentCoordinator = new PaymentsCoordinator(customerRegistry, (...))
  • 26.
    Mocks Creation @Mock private CustomerDataRegistrycustomerRegistry; (...) private PaymentsCoordinator paymentCoordinator; @Before public void setup() { MockitoAnnotations.initMocks(this); paymentCoordinator = new PaymentsCoordinator( customerRegistry, (...)); } CustomerDataRegistry customerRegistry = Mock() (...) @Subject PaymentsCoordinator paymentCoordinator = new PaymentsCoordinator(customerRegistry, (...))
  • 27.
    Responses declaration @Test public voidtestSuccessfulPaymentWithNewCreditCard() throws Exception { when(customerRegistry.getCustomerData(SUBSCRIPTION_ID)).thenReturn(CUSTOMER_DATA); (...) PaymentResult result = paymentCoordinator.doPayment(paymentData); (...) } def "Succesful payment with new credit card"() { given: "A registered customer" customerRegistry.getCustomerData(SUBSCRIPTION_ID) >> CUSTOMER_DATA (...) when: "A payment is requested" def result = paymentCoordinator.doPayment(paymentData) (...) }
  • 28.
    Interaction expectations @Test public voidtestSuccessfulPaymentWithNewCreditCard() throws Exception { (...) when(paymentInteractor.performPaymentInProvider(inputFields)) .thenReturn(PAYMENT_SUCCESSFUL_OUTPUT); PaymentResult result = paymentCoordinator.doPayment(paymentData); verify(paymentInteractor).performPaymentInProvider(inputFields)). (...) } def "Succesful payment with new credit card"() { (...) when: "A payment is requested" def result = paymentCoordinator.doPayment(paymentData) then: "It is sent to the payment provider with successful result" 1 * paymentInteractor.performPaymentInProvider(inputFields) >> PAYMENT_SUCCESSFUL_OUTPUT (...) }
  • 29.
    Mocks and Stubs then: 0* _ Semantic Lenient: default values Only return values Stubs: empty objects Stubs: no interaction expectations Mocks: nulls
  • 30.
    Call Matching Arguments matching mock.method("hello") mock.method(!"hello") mock.method() mock.method(_) mock.method(*_) mock.method(_as String) mock.method({ l -> l.size() > 3 }) Method matching customerRegistry._ _ customerRegistry./searchBy.*/(…)
  • 31.
    Interactions expectations matching Parametesmatching Order 1 * subscriber.receive("hello") 0 * subscriber.receive("hello") (1..3) * subscriber.receive("hello") (1.._) * subscriber.receive("hello") (_..3) * subscriber.receive("hello") _ * subscriber.receive("hello") then: 1 * mock1.method1(...) and: 1 * mock2.method2(...) then: 1 * mock3.calledAfter1And2() Cardinality * matching constraints
  • 32.
    Forget the matchers @Test publicvoid testSuccessfulPaymentWithNewCreditCard() throws Exception { (...) ArgumentMatcher<Payment> paymentMatcher = new ArgumentMatcher<Payment>() { public boolean matches(Object payment) { return ((Payment) payment).getPaymentType() != null; } }; verify(paymentInteractor, never()).performPaymentInProvider( paymentMatcher, anyObject(), eq(VALIDATE)); (...) } def "Succesful payment with new credit card"() { (...) then: 0 * paymentInteractor.performPaymentInProvider( { payment -> payment.storedCard != null }, _, VALIDATE) (...) }
  • 33.
  • 34.
    Property-based testing (JUnitparameterized tests) @RunWith(Parameterized.class) public class PaymentProcessingOfANewCompletedResponseAuthorizationHandlerTest { public static Object[][] data() { return new Object[][] { { TransactionStatus.INITIALIZED, TransactionStatus.AUTHORIZED }, { TransactionStatus.INITIALIZED, TransactionStatus.REFUSED }, { TransactionStatus.ACCEPTED, TransactionStatus.AUTHORIZED }, { TransactionStatus.ACCEPTED, TransactionStatus.REFUSED } }; } @Parameter public TransactionStatus currentStatus; @Parameter(value=1) public TransactionStatus responseStatus; @Test public void test() throws Exception { (...) } }
  • 35.
    Property-based testing (JUnitparameterized tests) @RunWith(Parameterized.class) public class PaymentProcessingOfANewCompletedResponseAuthorizationHandlerTest { public static Object[][] data() { return new Object[][] { { TransactionStatus.INITIALIZED, TransactionStatus.AUTHORIZED }, { TransactionStatus.INITIALIZED, TransactionStatus.REFUSED }, { TransactionStatus.ACCEPTED, TransactionStatus.AUTHORIZED }, { TransactionStatus.ACCEPTED, TransactionStatus.REFUSED } }; } @Parameter public TransactionStatus currentStatus; @Parameter(value=1) public TransactionStatus responseStatus; @Test public void test() throws Exception { (...) } }
  • 36.
    Property-based testing (JUnitparameterized tests) @RunWith(Parameterized.class) public class PaymentProcessingOfANewCompletedResponseAuthorizationHandlerTest { public static Object[][] data() { return new Object[][] { { TransactionStatus.INITIALIZED, TransactionStatus.AUTHORIZED }, { TransactionStatus.INITIALIZED, TransactionStatus.REFUSED }, { TransactionStatus.ACCEPTED, TransactionStatus.AUTHORIZED }, { TransactionStatus.ACCEPTED, TransactionStatus.REFUSED } }; } @Parameter public TransactionStatus currentStatus; @Parameter(value=1) public TransactionStatus responseStatus; @Test public void test() throws Exception { (...) } }
  • 37.
    Property-based testing (JUnitparameterized tests) @RunWith(Parameterized.class) public class PaymentProcessingOfANewCompletedResponseAuthorizationHandlerTest { public static Object[][] data() { return new Object[][] { { TransactionStatus.INITIALIZED, TransactionStatus.AUTHORIZED }, { TransactionStatus.INITIALIZED, TransactionStatus.REFUSED }, { TransactionStatus.ACCEPTED, TransactionStatus.AUTHORIZED }, { TransactionStatus.ACCEPTED, TransactionStatus.REFUSED } }; } @Parameter public TransactionStatus currentStatus; @Parameter(value=1) public TransactionStatus responseStatus; @Test public void test() throws Exception { (...) } }
  • 38.
    Property-based testing (Spockwhere) @Unroll def "Processing of a new completed response from status #currentStatus to #responseStatus"( currentStatus, responseStatus) { (...) when: def resultTransaction = authorizationHandler.processResponse(statusInfo, FROM_RETRY) then: (...) where: currentStatus | responseStatus TransactionStatus.INITIALIZED | TransactionStatus.AUTHORIZED TransactionStatus.INITIALIZED | TransactionStatus.REFUSED TransactionStatus.ACCEPTED | TransactionStatus.AUTHORIZED TransactionStatus.ACCEPTED | TransactionStatus.REFUSED }
  • 39.
    Property-based testing (Spockwhere) @Unroll def "Processing of a new completed response from status #currentStatus to #responseStatus"( currentStatus, responseStatus) { (...) when: def resultTransaction = authorizationHandler.processResponse(statusInfo, FROM_RETRY) then: (...) where: currentStatus | responseStatus TransactionStatus.INITIALIZED | TransactionStatus.AUTHORIZED TransactionStatus.INITIALIZED | TransactionStatus.REFUSED TransactionStatus.ACCEPTED | TransactionStatus.AUTHORIZED TransactionStatus.ACCEPTED | TransactionStatus.REFUSED }
  • 40.
    Config tests (multilineand String interpolation) def "getRejectionReasonForError with invalid reason mapped"() { given: def yaml = """ paymentForm: rejectionReasonMapping: ${ERROR_CODE}: '${REJECTION_REASON_INVALID}' """ def config = formConfig(yaml) when: def returnedReason = config.getRejectionReasonForError(ERROR_CODE) then: "Returns general error" returnedReason == PaymentRejectionReason.GENERAL_ERROR }
  • 41.
    Config tests (multilineand String interpolation) def "getRejectionReasonForError with invalid reason mapped"() { given: def yaml = """ paymentForm: rejectionReasonMapping: ${ERROR_CODE}: '${REJECTION_REASON_INVALID}' """ def config = formConfig(yaml) when: def returnedReason = config.getRejectionReasonForError(ERROR_CODE) then: "Returns general error" returnedReason == PaymentRejectionReason.GENERAL_ERROR }
  • 42.
    Builders? def payment =new PaymentTransaction( id: TRANSACTION_ID, subscriptionId: SUBSCRIPTION_ID, amount: AMOUNT, lastChangeTimestamp: OLD_TIMESTAMP) PaymentTransaction payment = new PaymentTransactionBuilder() .id(TRANSACTION_ID) .subscriptionId(SUBSCRIPTION_ID) .amount(AMOUNT) .lastChangeTimestamp(OLD_TIMESTAMP) .build(); Default constructor with named parameters
  • 43.
    Builders? def payment =new PaymentTransaction( id: TRANSACTION_ID, subscriptionId: SUBSCRIPTION_ID, amount: AMOUNT, lastChangeTimestamp: OLD_TIMESTAMP) PaymentTransaction payment = new PaymentTransactionBuilder() .id(TRANSACTION_ID) .subscriptionId(SUBSCRIPTION_ID) .amount(AMOUNT) .lastChangeTimestamp(OLD_TIMESTAMP) .build(); Default constructor with named parameters
  • 44.
    def "Process anauthorized payment "() { given: "An already authorized payment" def authorizedPayment = paymentTransactionWith(status: TransactionStatus.AUTHORIZED) (...) } private PaymentTransaction paymentTransactionWith(Map overrides) { def attrs = [ id: TRANSACTION_ID, subscriptionId: SUBSCRIPTION_ID, amount: AMOUNT, status: TransactionStatus.INITIALIZED, lastChangeTimestamp: OLD_TIMESTAMP ] + overrides return new PaymentTransaction(attrs) } Objects with default properties
  • 45.
    def "Process anauthorized payment "() { given: "An already authorized payment" def authorizedPayment = paymentTransactionWith(status: TransactionStatus.AUTHORIZED) (...) } private PaymentTransaction paymentTransactionWith(Map overrides) { def attrs = [ id: TRANSACTION_ID, subscriptionId: SUBSCRIPTION_ID, amount: AMOUNT, status: TransactionStatus.INITIALIZED, lastChangeTimestamp: OLD_TIMESTAMP ] + overrides return new PaymentTransaction(attrs) } Objects with default properties
  • 46.
    Private methods forreadability? PaymentTransaction transaction = givenAnAuthorizedTransaction(); paymentProcessor.processPayment(transaction, RESPONSE_DATA); verifyTheTransactionIsTransitionedTo(ADDING_BALANCE); verifyTheTopupIsProcessedCorrectly();
  • 47.
    Private methods forreadability? given: "An already authorized transaction" def transaction = transactionWith(status: AUTHORIZED) transactionRegistry.getTransactionInfo(TRANSACTION_ID) >> transaction when: "The payment is processed" paymentProcessor.processPayment(transaction, RESPONSE_DATA) then: "The transaction is transitioned to adding balance status" 1 * transactionRegistry.changeTransactionStatus( TRANSACTION_ID, TransactionStatus.ADDING_BALANCE) >> transactionWith(status: ADDING_BALANCE) then: "The topup is processed" 1 * topupInteractor.topup(transaction, RESPONSE_DATA) >> new PaymentStatusInfo(TRANSACTION_ID, TransactionStatus.BALANCE_ADDED) PaymentTransaction transaction = givenAnAuthorizedTransaction(); paymentProcessor.processPayment(transaction, RESPONSE_DATA); verifyTheTransactionIsTransitionedTo(ADDING_BALANCE); verifyTheTopupIsProcessedCorrectly();
  • 48.
    static final MERCADOPAGO_RESULT= [ status: 'approved', status_detail: 'ok', description: 'Tuenti (test)', id: 999999, authorization_code: 858, collector_id: 5678, statement_descriptor: 'WWW.MERCADOPAGO.COM', card: [ last_four_digits: 1234, expiration_year: 2016, expiration_month: 12 ], payer: [ first_name: NAME, last_name: LAST_NAME, email: EMAIL, ] ] Tests involving 3rd party APIs – Thank God for Collection literals
  • 49.
  • 50.
    ● Increase abstraction level Not“programming tests” ® specify test cases Easy + powerful Expressivity ® test is also documentation ● Easy to run in continuous integration systems / IDEs ● Better error detection info Advantages?
  • 51.
    ● Code Refactorsnot so safe ● Mocks can only be created in the Spec class Integration tests with dependency injection overrides ... more difficult, but possible! Disadvantages?
  • 52.
    ● Code Refactorsnot so safe ● Mocks can only be created in the Spec class Integration tests with dependency injection overrides ... more difficult, but possible! Disadvantages? class BaseIntegrationSpecification extends TIntegrationSpecification { @InjectOverride MercadopagoClient mercadopago = Mock() @Inject PaymentNotificationsService paymentNotificationsServiceMock (...) @TIntegrationTestsModule static class MockedBoundariesModule extends SpockMocksModule { (...) } }
  • 53.
    ● Code Refactorsnot so safe ● Mocks can only be created in the Spec class Integration tests with dependency injection overrides ... more difficult, but possible! Disadvantages? class BaseIntegrationSpecification extends TIntegrationSpecification { @InjectOverride MercadopagoClient mercadopago = Mock() @Inject PaymentNotificationsService paymentNotificationsServiceMock (...) @TIntegrationTestsModule static class MockedBoundariesModule extends SpockMocksModule { (...) } }
  • 54.
    Do you dareto change?
  • 55.
    Do you dareto change?
  • 56.

Editor's Notes

  • #2 Buenas a todos, gracias por venir. Java Testing ==&amp;gt; cuánta gente realmente prueba?
  • #5 - opensource, std. Groovy
  • #53 Fácil pensar – no te da tanto, ¿por qué cambiar? Ejemplo parecido – salto de altura Dick Fosbury – Mexico 68 Hasta años después no se popularizó del todo – quizá lo que pensaban entonces el resto de saltadores es que no te da tanto, que por qué cambiar, que ellos llevaban toooda la vida … saltando así.