Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to make your unit tests Spektacular

How to make your unit tests Spektacular

Mikolaj Leszczynski

February 21, 2018
Tweet

More Decks by Mikolaj Leszczynski

Other Decks in Technology

Transcript

  1. Our JUnit test structure @Test fun test_name() { //given set

    up context here (mocks, variables, prerequisites etc.) //when execute action here //then assertions go here }
  2. Context Hell @Test public void should_not_enable_pay_button_when_promotion_is_applied_and_there_is_nothing_to_pay_and_credit_card_is_required_and_the_ payment_card_widget_is_initialised_and_the_payment_details_widget_is_initialised_and_no_card_is_selected() { // given

    givenPaymentCardWidgetInitialised(); givenPaymentDetailsWidgetInitialised(); givenNoCardSelected(); paymentDetailsPresenter.takeView(mockView); int discountPercentage = 100; boolean creditCardRequired = true; . . . // when . . . // then . . . }
  3. Context Hell @Test public void should_not_enable_pay_button_when_promotion_is_applied_and_there_is_nothing_to_pay_and_credit_card_is_required() { // given givenPaymentCardWidgetInitialised();

    givenPaymentDetailsWidgetInitialised(); givenNoCardSelected(); paymentDetailsPresenter.takeView(mockView); int discountPercentage = 100; boolean creditCardRequired = true; PaymentPlan paymentPlan = createPaymentPlanBuilder().build(); Promotion promotion = createPromotionBuilder() .setCreditCardRequired(creditCardRequired) .setDiscountPercentage(discountPercentage) .setPaymentPlan(paymentPlan) .build(); Observable<Promotion> promoCodeObservable = Observable.just(promotion); // when paymentDetailsPresenter.onPromoCodeInitialised(promoCodeObservable); // then verify(mockView, never()).enablePayButton(); }
  4. Assertion Hell @Test public void should_return_specialists() { // given DoctorModel

    doctorModel = getDoctorModel(); when(mockDoctorsService.getAllSpecialists()).thenReturn(Single.just(Collections.singletonList(doctorModel))); // when TestObserver<List<DoctorSimple>> assertableSubscriber = retrofitDoctorsGateway.getDoctorsOfType(DoctorType.create(DoctorType.Type.SPECIALIST)).test(); // then assertableSubscriber.assertComplete(); List<List<DoctorSimple>> onNextEvents = assertableSubscriber.values(); assertThat(onNextEvents.size()).isEqualTo(1); DoctorSimple doctorSimple = onNextEvents.get(0).get(0); assertThat(doctorSimple.getDoctorType()).isEqualTo(DoctorType.create(DoctorType.Type.SPECIALIST)); assertThat(doctorSimple.getId()).isEqualTo(String.valueOf(doctorModel.getId())); assertThat(doctorSimple.getName()).isEqualTo(doctorModel.getName()); assertThat(doctorSimple.getAvatarUrl()).isEqualTo(doctorModel.getAvatar()); assertAll(); assertableSubscriber.assertNoErrors(); }
  5. Execution flow hierarchy Execution flow: @Before public void setUp() {}

    @Test public void test1() {} setUp —> test1 @Test public void test2() {} setUp —> test2 @Test public void test3() {} setUp —> test3 @Test public void test4() {} setUp —> test4
  6. BDD • Verbose in communication • Concise in code •

    Putting tests in context • Creating a specification for the test subject • Documenting the subject’s behaviour
  7. Spek syntax given("a patient") { // set up context here

    on("treating the patient") { // action goes here it("heals the patient") { // assertion (test) goes here } } }
  8. Spek syntax given("a patient") { // set up context here

    on("treating the patient") { // action goes here it("heals the patient") { // assertion (test) goes here } } }
  9. Spek syntax given("a patient") { // set up context here

    on("treating the patient") { // action goes here it("heals the patient") { // assertion (test) goes here } } }
  10. Spek syntax given("a patient") { // set up context here

    on("treating the patient") { // action goes here it("heals the patient") { // assertion (test) goes here } } }
  11. On fun compute() = 6 * 7 on("running the ultimate

    computation") { val actualAnswer = compute() it("should equal 42") { actualAnswer `should be` 42 } it("should be divisible by 2") { actualAnswer % 2 `should be` 0 } }
  12. Given given("the number 6") { val x = 6 given("the

    number 7") { val y = 7 on(“multiplying") { val actualAnswer = multiply(x, y) it("should equal 42") { actualAnswer `should be` 42 } } } }
  13. given("the number 2") { val x = 2 on("squaring") {

    val actualAnswer = squared(x) it("should equal 4") { actualAnswer `should be` 4 } } on(“cubing") { val actualAnswer = cubed(x) it("should equal 8") { actualAnswer `should be` 8 } } }
  14. State in tests val subject = SubjectUnderTest() it("checks the subject

    instance") { println("Instance: $subject") } it("checks the subject instance again") { println("Instance again: $subject") } Output Instance: randoms.SubjectUnderTest@3bb9a3ff Instance again: randoms.SubjectUnderTest@3bb9a3ff
  15. Memoized val subject by memoized { SubjectUnderTest() } it("checks the

    subject instance") { println("Instance: $subject") } it("checks the subject instance again") { println("Instance again: $subject") } Output Instance: randoms.SubjectUnderTest@59309333 Instance again: randoms.SubjectUnderTest@222545dc
  16. val subject by memoized { SubjectUnderTest() } on("action 1") {

    println("On instance 1: $subject") it("checks the subject instance") { println("It instance 1: $subject") } } on("action 2") { println("On instance 2: $subject") it("checks the subject instance again") { println("It instance 2: $subject") } } Output On instance 1: randoms.SubjectUnderTest@67d48005 It instance 1: randoms.SubjectUnderTest@67d48005 On instance 2: randoms.SubjectUnderTest@478190fc It instance 2: randoms.SubjectUnderTest@478190fc
  17. val subject by memoized { SubjectUnderTest() } given("context 1") {

    println("Given instance 1: $subject") it("checks the subject instance") { println("It instance 1: $subject") } } given("context 2") { println("Given instance 2: $subject") it("checks the subject instance again") { println("It instance 2: $subject") } } Output Given instance 1: randoms.SubjectUnderTest@6a28ffa4 Given instance 2: randoms.SubjectUnderTest@6a28ffa4 It instance 1: randoms.SubjectUnderTest@6a28ffa4 It instance 2: randoms.SubjectUnderTest@222545dc
  18. Due to how Spek is structured, group scopes are eagerly

    evaluated during the discovery phase. Any logic that needs to be evaluated before and/or after test scopes should be done using fixtures (…)
  19. val subject by memoized { SubjectUnderTest() } given("context 1") {

    beforeEachTest { println("Given instance 1: $subject") } it("checks the subject instance") { println("It instance 1: $subject") } } given("context 2") { beforeEachTest { println("Given instance 2: $subject") } it("checks the subject instance again") { println("It instance 2: $subject") } } Output Given instance 1: randoms.SubjectUnderTest@895e367 It instance 1: randoms.SubjectUnderTest@895e367 Given instance 2: randoms.SubjectUnderTest@2b72cb8a It instance 2: randoms.SubjectUnderTest@2b72cb8a
  20. Test execution order 1. Discovery - all given blocks are

    executed first 2. Execution: For each it: 1. beforeEachTest sections of containing givens 2. Containing on is executed 3. it is executed
  21. Inside a given: 1. To instantiate variables, always use memoized

    2. Everything else should be in beforeEachTest
  22. Our rules for well written speks 1. Describe the behaviour,

    not the implementation 2. One assertion per it 3. One action per on 4. One statement per given (ideally!) 5. Everything in a given should be in beforeEachTest or use memoized
  23. class CreatePasswordValidatorSpek : Spek({ val mockContext by memoized { mock<Context>()

    } val createPasswordValidator by memoized { CreatePasswordValidator(mockContext) } val INVALID_PASSWORD = "nonvalidpassword" val PATIENT_ID = "patient_id" val ERROR_MESSAGE = "message" given("an error message is returned") { beforeEachTest { whenever(mockContext.getString(any())).thenReturn(ERROR_MESSAGE) } given("a non valid password request") { val createPasswordRequest by memoized { CreatePasswordRequest.builder() .setPassword(INVALID_PASSWORD) .setPatientId(PATIENT_ID) .build() } on("validation") { val testObserver = createPasswordValidator.validate(createPasswordRequest).test() it("returns a validation exception") { testObserver.assertError(InvalidPasswordException::class.java) } it("sets the error message to \"$ERROR_MESSAGE\"") { testObserver.errors()[0].message `should be` ERROR_MESSAGE } } } } })
  24. I love how you can create a test skeleton for

    the whole class, and then write all the mocks and verifications in it
  25. The test outputs are just plain english and you can

    go straight to the error when it fails