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

Testing Business Logic Using Domain Specific L...

Avatar for Mayank Jain Mayank Jain
September 11, 2015

Testing Business Logic Using Domain Specific Languages (DSLs) in Clojure

Talk presented at FuConf'15
Video - https://www.youtube.com/watch?v=YOsfPrgNY_M

Avatar for Mayank Jain

Mayank Jain

September 11, 2015

More Decks by Mayank Jain

Other Decks in Programming

Transcript

  1. A Domain Specific Language (DSL) is a programming language designed

    specifically to express solutions to problems in a specific domain. DSL
  2. HELPSHIFT • Embeddable support desk for native apps • Main

    Features: • Frequently Asked Questions which customers can search • File issues/tickets from within the app
  3. Helpshift Supercell Clash of Clans Gaana General Billing Domain App

    Section FAQ 1 FAQ 2 FAQs Boom Beach Gaana App Translations English Content Hindi Content …. …. …. …. …. Top Down View of FAQs
  4. PROBLEM You cannot share FAQs across apps. Big Customers have

    multiple apps which have same FAQ translations content example “Privacy Policy”
  5. {:published? false :id “faq-id-1” :app_id “app-1” :section_id “section-1” :translations {:en

    {:published? false :stags [] :body “Privacy Body” :title “Privacy Title”}} :linked_faq_ids [“faq-id-2”] :publish_id “1”} App-1 {:published? false :id “faq-id-2” :app_id “app-2” :section_id “section-2” :translations {:en {:published? false :stags [] :body “Privacy Body" :title “Privacy Title"}} :linked_faq_ids [“faq-id-1”] :publish_id “2”} App-2 faq 1 (Sync faq-1 to App-2) General faq 2 General
  6. {:published? false :id “faq-id-1” :app_id “app-1” :section_id “section-1” :translations {:en

    {:published? false :stags [] :body “Update Body” :title “Privacy Title”}} :linked_faq_ids [“faq-id-2”] :publish_id “1”} {:published? false :id “faq-id-2” :app_id “app-2” :section_id “section-2” :translations {:en {:published? false :stags [] :body “Privacy Body" :title “Privacy Title"}} :linked_faq_ids [“faq-id-1”] :publish_id “2”} faq 1 Sync faq 2 Update FAQ 1’s Body
  7. {:published? false :id “faq-id-1” :app_id “app-1” :section_id “section-1” :translations {:en

    {:published? false :stags [] :body “Update Body” :title “Privacy Title”}} :linked_faq_ids [“faq-id-2”] :publish_id “1”} {:published? false :id “faq-id-2” :app_id “app-2” :section_id “section-2” :translations {:en {:published? false :stags [] :body “Update Body" :title “Privacy Title"}} :linked_faq_ids [“faq-id-1”] :publish_id “2”} faq 1 Sync faq 2 Update FAQ 1’s Body
  8. 1. Add 1st App with only English languages. 2. Add

    2nd App with only English languages 3. Add 1st FAQ under 1st App 4. Link 1st FAQ to 2nd App to create 2nd FAQ 5. Check -> translations of 2nd FAQ == 1st FAQ 6. Update English title of FAQ-1 7. Check -> translations of 2nd FAQ == 1st FAQ 8. Delete FAQ-1 9. Check if 1st FAQ is deleted in DB 10. Assert 2nd FAQ should remain as it is in database. Simulation Verification
  9. PARTS OF EACH ACTION “Add APP-1 with only English language”

    1. Type of Action Add an App 2. Names App-1 3. Arguments English Language 4. Expected Result As per Spec/Modal of the system 5. Actual Result Database
  10. EXPRESS ACTIONS AS DATA USING DEFRECORD “Add app APP-1 with

    English language” (->AddApp :app-1 {:langs-config [:en]})
  11. SERIES OF ACTIONS [(->AddApp :app-1 {:langs-config nil}) (->AddFaq :faq-1 :app-1

    <faq-config> <faq-translations>) (->AddApp :app-2 {:langs-config nil}) (->LinkFaq :faq-1 :faq-2 :app-2 {}) (->UpdateFaq :faq-2 :app-2 {} <new-translations>}) (->DeleteFaq :faq-1 {})]
  12. DSL CONTEXT {:env {:apps {:app-1 {:app-id “970"} :app-2 {:app-id “142”}}}

    :faqs {:faq-1 {:faq-id “112”}}}} :latest-simulated-state {….} :log [{:action-taken :add-faq :simulated-state {….} :actual-mongo {….} :actual-es {….}} {…more}]} Environment (:env) - Contains bindings of vars
  13. DSL CONTEXT :latest-simulated-state - Current Generated Expected State {:env {:apps

    {:app-1 {:app-id “970"} :app-2 {:app-id “142”}}} :faqs {:faq-1 {:faq-id “112”}}}} :latest-simulated-state {….} :log [{:action-taken :add-faq :simulated-state {….} :actual-mongo {….} :actual-es {….}} {…more}]}
  14. DSL CONTEXT Result (:log) - Contains Actual And Expected Result

    Immutable snapshot of action in time {:env {:apps {:app-1 {:app-id “970"} :app-2 {:app-id “142”}}} :faqs {:faq-1 {:faq-id “112”}}}} :latest-simulated-state {….} :log [{:action-taken :add-faq :simulated-state {….} :actual-mongo {….} :actual-es {….}} {…more}]}
  15. HIGH LEVEL OVERVIEW OF DSL run-dsl Initial Context First Action

    Context After First Action run-dsl Second Action Context After Second Action And so on… Context After First Action
  16. RUN-DSL run-dsl {:env {} :latest-simulated-state {} :log []} (->AddApp :app-1

    {:langs-config nil}) {:env {:apps {:app-1 {…}}} :log [{log-1}] :latest-simulated-state {}} …..} run-dsl (->AddApp :app-2 {:langs-config nil}) {:env {:apps {:app-1 {…} :app-2 {…}}} :log [{log-1} {log-2}] :latest-simulated-state {}} …..} And so on… {:env {:apps {:app-1 {…}}} :log [{log-1}] :latest-simulated-state {}}
  17. Dispatch On Action Update The Variables in Action to its

    bindings Call args with relevant function Update the result to the given var in env Update latest simulate state Log Actual DB State and Updated Simulate State resolve-action-vars run-action! update-env update-latest-simulate-state update-log
  18. Each Defrecord Action implements the following DSL Protocol functions 1.

    resolve-action-vars 2. run-action! 3. update-env 4. update-latest-simulate-state 5. update-log
  19. Dispatch On Action resolve-action-vars run-action! update-env update-latest-simulate- state update-log (->AddApp

    :app-1 {:langs-config nil}) (->AddApp :app-1 {:langs-config nil}) (add-app {:langs-config nil}) {:env {:apps {:app-1 {:app-id “970"}}} :latest-simulated-state {} :log []} {:env {:apps {:app-1 {:app-id “970"}}} :latest-simulated-state {} :log []} {:env {:apps {:app-1 {:app-id “970"}}} :latest-simulated-state {} :log []}
  20. Dispatch On Action resolve-action-vars run-action! update-env update-latest-simulate- state update-log (->AddApp

    :app-2 {:langs-config nil}) (->AddApp :app-2 {:langs-config nil}) (add-app {:langs-config nil}) {:env {:apps {:app-1 {:app-id “970”} :app-2 {:app-id “142”}} :latest-simulated-state {} :log []} {:env {:apps {:app-1 {:app-id “970”} :app-2 {:app-id “142”}} :latest-simulated-state {} :log []} {:env {:apps {:app-1 {:app-id “970”} :app-2 {:app-id “142”}} :latest-simulated-state {} :log []}
  21. Dispatch On Action resolve-action-vars (->AddFaq :faq-1 :app-1 {…} {:en {…}}))

    {:env {:apps {:app-1 {:app-id “970"} :app-2 {:app-id “142”}}}
  22. Dispatch On Action resolve-action-vars (->AddFaq :faq-1 :app-1 {…} {:en {…}}))

    (->AddFaq :faq-1 :app-1 {:app-id “970”} {:en {}}]) {:env {:apps {:app-1 {:app-id “970"} :app-2 {:app-id “142”}}}
  23. Dispatch On Action resolve-action-vars run-action! update-env (->AddFaq :faq-1 :app-1 {…}

    {:en {…}})) (->AddFaq :faq-1 :app-1 {:app-id “970”} {:en {}}]) (add-faq {:app-id “970”} {:en {…}})
  24. Dispatch On Action resolve-action-vars run-action! update-env (->AddFaq :faq-1 :app-1 {…}

    {:en {…}})) (->AddFaq :faq-1 {:app-id “970” …} {:en {…}}]) (add-faq {:app-id “970”} {:en {…}}) {:env {:apps {:app-1 {:app-id “970"} :app-2 {:app-id “142”}} :faqs {:faq-1 {:faq-id “112”}}}}
  25. Dispatch On Action resolve-action-vars run-action! update-env update-latest-simulate- state (->AddFaq :faq-1

    :app-1 {…} {:en {…}})) (->AddFaq :faq-1 {:app-id “970” …} {:en {…}}]) (add-faq {:app-id “970”} {:en {…}}) {:env {:apps {:app-1 {:app-id “970"} :app-2 {:app-id “142”}} :faqs {:faq-1 {:faq-id “112”}}}} {:env {:apps {:app-1 {:app-id “970"} :app-2 {:app-id “142”}}} :faqs {:faq-1 {:faq-id “112”}}} :latest-simulated-state {<Expected>}}
  26. Dispatch On Action resolve-action-vars run-action! update-env update-latest-simulate- state update-log {:env

    {:apps {:app-1 {:app-id “970"} :app-2 {:app-id “142”}}} :faqs {:faq-1 {:faq-id “112”}}} :latest-simulated-state {<Expected>} :log [{:action-taken :add-faq :actual-es <Actual> :actual-mongo <Actual> :simulated-state <Expected>}]
  27. COMPARE RESULT FOR FIRST ACTION {"112" {:translations {:en {:published? false,

    :stags [], :body "Temp body", :title “Title 1"}}, :linked-faq-ids #{}}} Actual Data in DB Expected Data from my Generated result {"112" {:translations {:en {:published? false, :stags [], :body "Temp body", :title “Title 1"}}, :linked-faq-ids #{}}} == PASS
  28. {"112" {:translations {:en {:published? false, :stags [], :body "Temp body",

    :title "Title 1"}}, :linked-faq-ids #{}}} Actual Data in DB Expected Data from my Generated result {"112" {:translations {:en {:published? false, :stags [], :body "Temp body", :title "Title 2"}}, :linked-faq-ids #{}}} == FAIL COMPARE RESULT FOR SECOND ACTION
  29. APPROACHES TO VERIFY Actions : Hardcoded Expected Output : Hardcoded

    Actions : Simulated Expected Output : Hardcoded Actions : Simulated Expected Output : Generated Actions : Generated Expected Output : Generated
  30. Hardcoded Actions, Hardcoded Expected Output ADVANTAGES DISADVANTAGES Requires no extra

    knowledge to understand Very cumbersome to enumerate Anyone can add/edit tests Modification is hard False negatives are not possible
  31. Simulate list of Actions, Hardcoded Final Expected Output ADVANTAGES DISADVANTAGES

    Easy to enumerate Requires knowledge of the DSL Very readable Maintenance overhead of DSL Shareable with dev to simulate bugs Does not check intermediate state False negatives are not possible Expected output may have data which is available only at runtime like faq-ids
  32. Simulate list of actions, Generate Expected Output ADVANTAGES DISADVANTAGES Easy

    to enumerate Requires knowledge of the DSL Very readable Maintenance overhead of DSL Shareable with dev to simulate bugs Maintenance overhead of Expected Modal Checks intermediate state False negatives are possible Runtime data is available like faq-ids
  33. Generate Actions, Generate Expected Output ADVANTAGES DISADVANTAGES Possible to generate

    large number of tests Requires knowledge of the DSL Possible to Shrink failed test case using test.check library Maintenance overhead of DSL Shareable with dev to simulate bugs Maintenance overhead of Expected Modal Checks intermediate state False negatives are possible Maintenance overhead of generative code for list of actions.
  34. Issue Audit Trail • Maintains a log of • Who

    Took an action • What Action they took
  35. What - Types of Actions App User • Create Issue

    • Reply Issue • etc… Admin User • Create Issue • Reply Issue • Resolve Issue • Edit Tags • etc… Agent User • Create Issue • Reply Issue • Resolve Issue • Edit Tags • etc…
  36. (let [r (simulate-actions [[:user :create-issue {:domain “mayanklocal", :message “0” :hs-tags

    [“ux" “bug ""game"] :platform-type :ios}] [:agent :change-status {:status :resolved}] [:admin :edit-tags {:tags ["paid" "crash" "bug"]}] [:user :reply {:message-type :confirmation-rejected}] [:user :reply {:message “1”}] [:agent :change-status {:status :resolved}] [:admin :edit-tags {:tags ["paid" "bug" "free"]}] [:user :reply {:message “2”}] [:user :reply {:message “3”}] [:agent :edit-tags {:tags ["paid" "crash"]}]])] (run-trails-tests* r) => true (run-messages-tests domain r) => true) Original Input Data for Edge Case
  37. (fact "Simpler Edge case failure" (let [response (simulate-actions [[:user :create-issue

    {:domain "mayanklocal" :platform-type :ios :message "0"}] [:agent :change-status {:status :resolved}] [:user :reply {:message "1"}] [:user :reply {:message "2"}] [:user :reply {:message "3"}] [:user :reply {:message "4"}] [:user :reply {:message "5"}] [:user :reply {:message "6"}] [:user :reply {:message "7"}] [:user :reply {:message "8"}] [:user :reply {:message "9"}] [:user :reply {:message "10"}] [:user :reply {:message "11"}] [:user :reply {:message "12"}]])] Shrink Input Data for Edge Case
  38. Bug • Pagination for older messages happens after 15 messages

    • This bug happens when two conditions are met • More than 15 messages to load • Certain audit trail appears at the border of pagination.
  39. ADVANTAGES OF WRITING DSL Discovering very hard to find bugs,

    for example we found: Duplicate issue messages being rendered only if the number of messages were "just right". Finding ordering bugs. Increase in developer/tester productivity
  40. DISADVANTAGES OF WRITING DSL Cost of maintaining DSLs is high.

    If your DSL is just data being evaluated at run time, changes in feature code will not throw any compile time errors like function parameters being changed in DSL code. You have to educate your team members to learn your specific DSL for that feature to be able to understand the tests.
  41. CONCLUSION DSL can be used as a workflow for writing

    tests. Separating simulation vs verification of tests. A step towards ability to generate tests instead of writing them.
  42. Further Resources • Presentation on Domain Specific Languages by Martin

    Fowler • DSLs in Action by Debasish Ghosh • Growing a DSL with Clojure • Jeanine Adkisson - Design and Prototype a Language In Clojure • Clojure Tutorials on DSLs with Tim Baldridge (Paid) • John Hughes - Testing the Hard Stuff and Staying Sane • Reid Draper - Powerful Testing with test.check • Clojure Made Simple - Rich Hickey