Test Driven Development in the Database How I Do It · – Roy Osherove « Your test should test...

Post on 12-Mar-2020

2 views 0 download

Transcript of Test Driven Development in the Database How I Do It · – Roy Osherove « Your test should test...

Test-Driven Development in the Database

– How I Do It

vidar.eidissen@eritec.no @NiceTheoryVidar

Vidar Eidissen

Short bio• Oracle Database Consultant / Developer / (DBA)

• Oracle databases since 1998

• 23 years @ the biggest EHR-vendor in Norway;DIPS ASA - 80.000+ users in 3 of 4 health regions

• Consulting since 2016

• ETL, performance, db development

• Application performance troubleshooting

• #SmartDB/#PinkDB-afficionado

June 20th @ 23:30

Karsten Wallin Kjetil Strønen Vidar Eidissen Lasse Jenssen ♠

Why am I here?

• I've wanted to see a presentation on this subject for several years

• Nobody did it

• So: Here I am…

Presentation content

Tools and frameworks

Coding techniques

some

most – independent of tools

Continuous Integration nothingContainers

Context for this presentation

Business Logic

External API

Low level API

Business Logic

External API

Low level API

Business Logic

External API

Low level API

Business Logic

External API

Low level API

Business Logic

External API

Low level API

Database code

Table Table Table Table Table

Front end servers

Clients

SmartDB

Writing code the regular way• Design

• Write code

• Inspect

• Do some manual testing

• Fix bugs

• Test some more

• But you only test the one thing that you changed…

Agile

Embrace change!

Writing code the regular way• Design

• Write code

• Inspect

• Do some manual testing

• Fix bugs

• Test some more

• But you only test the one thing that you changed…

The process needs improvement

Test Driven Design

• TDD definition:

• Requirements are turned into very specific test cases

• TDD encourages simple designs and inspires confidence - Kent Beck

Source: Wikipedia

The TDD process1. Add a test

2. Run all tests and see if the new test fails

3. Write the code (quickly to solve the case)

4. Run tests

5. Refactor code

6. Repeat Source: Wikipedia

Hence…

You should not write new code before you have a failing test

However…

I always write my procedure interface first

Why?

Because I can then use autocomplete in my IDE when I write the test

But I always write a failing test and run it

This ensures that the test actually fails and is being run

This acts as a canary bird so I don’t forget to declare the test in the package header

Naming of tests• <what_procedure/function>_<with_what_input>_<expected_result>

• I don't name them as test_<something>

• That it is a test is given from the context

• I can immediately see what the test is about and what has failed

• Naming like this is a challenge on 11g and below

• The possibilities for long identifiers in 12c and above is a bliss

Think of test names as

a requirement of your system

Note to self: Show basic example

I’m not totally useless…

– I can be used as a bad example

Examples; test-names

provision_access_for_reservation_with_valid_reservation_inserts_access_records

provision_access_for_reservation_with_valid_reservation_sets_correct_access_times

approve_membership_without_privileges_raises_exception

approve_membership_as_org_admin_approves_membership

approve_membership_as_approver_approves_membership

request_membership_with_auto_approvable_mail_domain_auto_approves_request

--%test procedure reserve_for_booking_with_null_user_given_books_for_current_user;

--%test procedure reserve_for_booking_with_payable_resource_creates_order_with_credit_card_payment;

--%test --%throws(-20000) procedure booking_with_cc_payment_and_no_payment_reference_raises_exception;

--%test procedure booking_with_cc_payment_and_confirmation_with_payment_reference_confirms_booking;

--%test procedure confirm_booking_with_cc_order_removes_open_payment_request;

--%test procedure confirm_payment_with_cc_order_creates_invoice;

--%test procedure confirm_payment_with_cc_order_removes_open_payment_request;

--%test procedure cancel_order_for_membership_cancels_membership_request;

--%test procedure cancel_order_for_booking_cancels_pending_booking;

Sometimes I break the patternreject_membership_rejects_membership

approve_membership_creates_correct_vouchers

I’m not saying it’s correct to break the pattern like this.

– I’m just observing my own code.

These could/should have been named otherwise.

Test-examples

procedure reserve_for_booking_with_no_payment_sets_booking_status_to_tentative is l_reservation_id number; l_start timestamp with time zone; l_end timestamp with time zone; l_reservation p_reservation.reservation_t; begin l_start := next_monday_at('09:00'); l_end := l_start + interval '30' minute; set_slot_attributes_for_resource(resource_id_in => c_minute_based_resource, price_in => 0, slot_size_in => 15); l_reservation_id := p_booking.reserve_for_booking(resource_id_in => c_minute_based_resource, service_id_in => null, user_id_in => p_user_session.user_id, from_time_in => l_start, to_time_in => l_end); l_reservation := p_reservation.reservation(l_reservation_id); ut.expect(l_reservation.net_price).to_(equal(0)); ut.expect(l_reservation.gross_price).to_(equal(0)); ut.expect(l_reservation.status).to_(equal(p_booking.c_status_reserved)); end;

next_monday_at(’09:30') would have given better readability

I try to use my APIs instead of writing SQL statements in tests. Improves

maintainability

function next_monday_at(hh24mi_in in varchar2) return timestamp with time zone is l_tz timestamp with time zone; begin l_tz := to_timestamp_tz(to_char(trunc(sysdate + 7, 'iw'), 'yyyy-mm-dd') || ' ' || hh24mi_in || ':00 +02:00', 'yyyy-mm-dd hh24:mi:ss tzh:tzm'); return l_tz; end;

Redundant. Could have done just return …

Does not account for daylight savings time

– Roy Osherove

« Your test should test one thing and one thing only »

Define "one thing"

• It’s usually not a single attribute

• It’s about the state after completing an execution of the code you’re testing

Test data for unit-testing• My goal is to prove correctness of my code

• I don't use production data

• I don't need to use a production size database either

• Performance testing is a different thing

• The important thing is to know my data

• I just need to have sufficient data to run the test-cases

Tools

• Different IDEs have different support for test-building

• I prefer not to use these for several reasons

• Binds you to the IDE

• Can be difficult to set up automated testing

• Often incurs a bit of mouse clicking

utPLSQL v3• http://utplsql.org

• https://github.com/utPLSQL/utPLSQL

• Originally written by Steven Feuerstein

• Rewritten by Jacek Gebal and several others

• Very easy to get started

• Good integration with CI-tools

When my code doesn't work as expected

• If the test-result (failed assert) doesn’t convey what’s wrong, I go to my logs

• If I don’t get an understanding of the problem, I improve my logs

• Stepping through the code is my last resort (and gives me a sense of failure)

• In production, logs document what happened.

• Getting access to production to step through code might not be that easy. Or appropriate.

• Fixing errors based on TDD can improve your logging/instrumentation skills

Coding style

Commits

Business Logic

External API

Low level API

Business Logic

External API

Low level API

Business Logic

External API

Low level API

Business Logic

External API

Low level API

Business Logic

External API

Low level API

Database code

Table Table Table Table Table

Front end servers

Clients

SmartDB

Readability

( Clean code )

Encapsulation

Exception handling

Use helper functions– For readability

– For shorter code (DRY)

Maintaining test-code• Test code has to be maintained just like regular code

• Not writing or maintaining them creates a debt that will have to be paid off later when I wish I had them

• Therefore, I try to keep my test-cases short, simple and understandable

• When I write code to check my result, I try to not write new code

• Use my own API’s as far as possible instead of writing new selects

• Delete obsolete tests

Stay calm

• I don’t test everything

• I don’t keep track of code coverage

• Although it might be useful i larger teams

• I don’t loose sleep over missing tests

• I’m more disturbed by badly tested code

Staying on top

• When a bug is reported, I try to write a test replicating the bug

• For less well defined bugs, this may be difficult - so I troubleshoot first

• Retrofitting tests on old code is time consuming and feels counter-productive

TDD doesn’t find all errors

• Had a bug where a search service returned invalid results

• No unit-tests were able to unveil the problem

• Turned out the client used different inputs for the two service calls

Remember

Your tests are an excellent tool for you and your successors to manage

and change your legacy code

MentalitySpeed

ToolsPerformance

Performance?

TDD encourages simple designs and inspires confidence

I like encapsulation

• I like to build top-down, then bottom-up and work in waves like that

• I try to keep my interfaces "narrow" - not exposing my inner needs

• If it’s "correct"?

• I don’t know

• But that’s often how I work out additional requirements (context) in the input

As a result

• I have a tendency to create methods taking id’s as inputs

• This often makes the units easy to test (less code)

• It can have a performance downside - but is it important?

• Since I instrument my client calls with method and action, I can easily determine if the methods need a performance review when traffic picks up

procedure book_hour_based_resource_with_valid_resource_and_time_creates_booking is l_booking p_booking.booking_t; l_booking_id number; l_start_time timestamp with time zone := next_monday_at('14:00'); l_end_time timestamp with time zone := next_monday_at('16:00'); l_adjusted_end_time timestamp with time zone; begin l_booking_id := p_booking.reserve_for_booking(resource_id_in => c_hour_based_resource, service_id_in => null, user_id_in => p_user_session.user_id, from_time_in => l_start_time, to_time_in => l_end_time); l_booking := p_booking.get(booking_id_in => l_booking_id); ut.expect(l_booking.resource_id).to_(equal(c_hour_based_resource)); ut.expect(l_booking.service_id).to_(be_null); ut.expect(l_booking.start_time).to_(equal(l_start_time)); ut.expect(l_booking.end_time).to_(equal(l_end_time)); end;

Rename to"booking"?

Could have returned a booking_t-type

A PL/SQL record type

A better example l_invoice_org_id := get_invoice_org_id(user_id_in => l_user_id, resource_in => l_resource_id); l_display_end_time := calculate_end_time(l_resource_id, to_time_in);

resource_is_bookable_in_given_time(l_resource_id, l_user_id, from_time_in, to_time_in); l_initial_price := resource_net_price(l_resource_id, l_user_id, from_time_in, to_time_in);

Yes – of course! These functions and procedures where looking up the

resource in their internal implementation

An easy fix l_resource p_resource.resource_t;begin

l_resource := p_resource.resource(resource_id_in => resource_id_in); l_invoice_org_id := get_invoice_org_id(user_id_in => l_user_id, resource_in => l_resource); l_display_end_time := calculate_end_time(l_resource, to_time_in); resource_is_bookable_in_given_time(l_resource, l_user_id, from_time_in, to_time_in); l_initial_price := resource_net_price(l_resource, l_user_id, from_time_in, to_time_in);

user_id may still be a case

Anyway…

– Donald Knuth

The real problem is that programmers have spent far too much time

worrying about efficiency in the wrong places and at the wrong times;

premature optimization is the root of all evil

(or at least most of it) in programming.

Watch Kevlin Henney’s presentation on JavaZone 2018:

Structure and Implementation of Test Cases

https://player.vimeo.com/video/289852238

Time for questions?

Thank you for your attention!

Blog: nicetheory.io

Twitter: @NiceTheoryVidar

Email: vidar.eidissen@eritec.no