Post on 12-Mar-2020
transcript
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