Skip to content

Commit

Permalink
WIP: functional testing (#17)
Browse files Browse the repository at this point in the history
* WIP: functional testing

- mocking out tests and then working on implementing them leveraging
  migratus to avoid state issues

* initial workup of mocking requests and scaffolding out test

- wrote a mocking function for dealing with requests generation for
  passing to handlers. initially pulled in ring-mock, but for now my own
  cs-http-request-mock is more suited to what i need
- lein test and cider test aren't both using the test profile so i'm
  manually defining the test db for now in the core test file
- my first list creation test is working well with my mock funtion
- i've defined mock tests for the other crud operations with comments,
  will tackle those next

* bugfix: create queries were returning id of 0

needed to update the list and item execute query meta data to be :? if
they are returning anything.

reference: layerware/hugsql#15 (comment)

* testing update:

- full ci pipeline test runs can be run with the test profile and
  specific database; and driven by a run-tests.sh shell script
- removed pieces trying to manipulate the db path already defined once
  cider is running; i'll just be ok with cider tests running in the
  dev profile
- updated base-mock to allow for params or route-params if defined
- added uuid->str function for use in tests
- wrote and have working tests for list deletion and item creation

* tests update:

- added read-item query to get the details of a single item
- utility function to convert from hugsql clojure map uuid to java uuid format
- reworded tests to assert truthiness when they pass
- wrote check, uncheck, and item delete tests
- updated tests to not only check for a successful 302 but verified values or deletion as well

* update the docs for queries returning an id

* refactor helper functions into a util namespace

* test refactor:

created util functions for generating test lists and items returning
maps of needed ids in the desired formats.

used new functions in tests and referenced the keys in the returned
maps.  this is a lot more readable and changing the logic can be done
in a single place now as long as the data returned is a map which
contains at least the expected keys.

* test pipeline update:

- updated to circleci v2
- added in container setup for postgres test

* setting db name at image load

* set db_url for test in ci file

* update circle ci file with db config

* ci update / fix:

- pull out pg vars
- make lein use test profile

* ci update:

- bumping up postgres
- removing db url as it's redundant

* ci debug

* ci debug

* debug ci

* debug ci

* debug ci

* ci debug

* debug ci

* ci debug

* ci debug

* ci debug

* post debug: cleaning up ci file

* ci update: add test run and uberjar build back in

* build / deploy update:

- converted deploy process to v2.0
- due to no heroku integration yet had to follow the docs here:
https://circleci.com/docs/2.0/deployment_integrations/#heroku
- minor ci fix
- updated run-tests for local use which properly sets up and cleans up

* fix working directory issue

* deploy fix: added checkout step

* minor test comment update
  • Loading branch information
chadhs committed Dec 28, 2017
1 parent df90d71 commit 0f548ce
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 38 deletions.
96 changes: 96 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
version: 2
jobs:
build:
working_directory: ~/listopia
docker:
- image: circleci/clojure:lein-2.7.1
environment:
DATABASE_URL: postgresql://listopia@localhost/listopia-test
- image: circleci/postgres:9.6-alpine
environment:
POSTGRES_USER: listopia
POSTGRES_DB: listopia-test
POSTGRES_PASSWORD: ""
environment:
LEIN_ROOT: nbd
JVM_OPTS: -Xmx3200m
steps:
- checkout
- restore_cache:
key: listopia-{{ checksum "project.clj" }}
- run: lein deps
- save_cache:
paths:
- ~/.m2
- ~/.lein
key: listopia-{{ checksum "project.clj" }}
- run:
name: wait for db
command: dockerize -wait tcp://localhost:5432 -timeout 1m
- run:
name: database initialization
command: lein migratus migrate
- run:
name: run tests
command: lein test
- run:
name: build uberjar
command: lein uberjar
- store_artifacts:
path: target/listopia.jar
destination: uberjar

deploy-stage:
working_directory: ~/listopia
docker:
- image: circleci/clojure:lein-2.7.1
steps:
- checkout
- run:
name: setup heroku
command: bash .circleci/setup-heroku.sh
- add_ssh_keys:
fingerprints:
- "ad:5b:ff:2c:db:5c:c7:c3:a2:0b:a4:6e:53:3d:3b:21"
- run:
name: deploy staging to heroku
command: |
git push git@heroku.com:listopia-staging.git $CIRCLE_SHA1:refs/heads/master
heroku run -a listopia lein migratus migrate
deploy-prod:
working_directory: ~/listopia
docker:
- image: circleci/clojure:lein-2.7.1
steps:
- checkout
- run:
name: setup heroku
command: bash .circleci/setup-heroku.sh
- add_ssh_keys:
fingerprints:
- "ad:5b:ff:2c:db:5c:c7:c3:a2:0b:a4:6e:53:3d:3b:21"
- run:
name: deploy master to heroku
command: |
git push git@heroku.com:listopia.git $CIRCLE_SHA1:refs/heads/master
heroku run -a listopia lein migratus migrate
workflows:
version: 2
build-deploy:
jobs:
- build
- deploy-stage:
requires:
- build
filters:
branches:
only: staging
- deploy-prod:
requires:
- build
filters:
branches:
only: master
16 changes: 16 additions & 0 deletions .circleci/setup-heroku.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
wget https://cli-assets.heroku.com/branches/stable/heroku-linux-amd64.tar.gz
sudo mkdir -p /usr/local/lib /usr/local/bin
sudo tar -xvzf heroku-linux-amd64.tar.gz -C /usr/local/lib
sudo ln -s /usr/local/lib/heroku/bin/heroku /usr/local/bin/heroku

cat > ~/.netrc << EOF
machine api.heroku.com
login $HEROKU_LOGIN
password $HEROKU_API_KEY
EOF

cat >> ~/.ssh/config << EOF
VerifyHostKeyDNS yes
StrictHostKeyChecking no
EOF
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ create a `profiles.clj` in the project root (which is ignored by git) to define
{:dev {:env {:database-url "jdbc:postgresql://localhost/listopia-dev"}}}
```

to run locally use `lein run` this will by default start on port 8000, but accepts a port number as an argument as well.
to run locally use `lein run` this will start on port 8000 by default, but accepts a port number as an argument as well.

## database migrations

Expand All @@ -33,7 +33,7 @@ to run locally use `lein run` this will by default start on port 8000, but accep

## production setup

you'll need to load an two environment variables in your production environment:
you'll need to load two environment variables in your production environment:

`DATABASE_URL` and `PORT`

Expand Down
27 changes: 0 additions & 27 deletions circle.yml

This file was deleted.

6 changes: 6 additions & 0 deletions run-tests-local.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

[[ -n $(psql -l | grep listopia-test) ]] && dropdb listopia-test
createdb listopia-test
lein with-profile test migratus migrate && lein test
dropdb listopia-test
10 changes: 8 additions & 2 deletions src/listopia/item/sql/item.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
-- listopia list item queries


-- :name create-item! :! :n
-- :doc insert a single list item
-- :name create-item! :? :n
-- :doc insert a single list item, returning the id, thus the ? in the name rather than ! for execute
insert into item (name, description, list_id)
values (:name, :description, :list-id)
returning id
Expand All @@ -28,6 +28,12 @@ delete from item
where list_id = :list-id


-- :name read-item :? :n
-- :doc get an item by id
select id, name, description, checked, date_created from item
where id = :item-id


-- :name read-items :? :*
-- :doc get all list items
select id, name, description, checked, date_created from item
Expand Down
4 changes: 2 additions & 2 deletions src/listopia/list/sql/list.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
-- listopia list queries


-- :name create-list! :! :n
-- :doc insert a single list
-- :name create-list! :? :n
-- :doc insert a single list, returning the id, thus the ? in the name rather than ! for execute
insert into list (name, description)
values (:name, :description)
returning id
Expand Down
59 changes: 59 additions & 0 deletions src/listopia/util/core.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
(ns listopia.util.core
(:require [listopia.list.model :as list.model]
[listopia.item.model :as item.model]))


(defn http-request-mock
"creates a request defaulting to http"
[& {:keys [scheme server-port uri request-method params route-params]
:or {scheme :http server-port 80 request-method :get}}]
(let [base-mock {:protocol "HTTP/1.1"
:scheme scheme
:server-port server-port
:server-name "localhost"
:remote-addr "localhost"
:headers {"host" "localhost"}
:uri uri
:request-method request-method}]
(cond-> base-mock
params (assoc :params params)
route-params (assoc :route-params route-params))))


(defn uuid->str
"return the plain string value of a given uuid."
[uuid]
(str (uuid :id)))


(defn hugsqluuid->javauuid
"return the java.util.UUID/fromString uuid format from a hugsql uuid map format."
[uuid]
(uuid :id))


(defn generate-test-list
"generates a test list returning a map of list-id, list-id-str, and list-id-uuid"
[db]
(let [list-id (list.model/create-list! db {:name "foo" :description "bar"})
list-id-str (uuid->str list-id)
list-id-uuid (hugsqluuid->javauuid list-id)]
{:list-id list-id
:list-id-str list-id-str
:list-id-uuid list-id-uuid}))


(defn generate-test-item
"generates a test item returning a map of item-id, item-id-str, and item-id-uuid"
[db]
(let [test-list (generate-test-list db)
list-id-str (get test-list :list-id-str)
list-id-uuid (get test-list :list-id-uuid)
item-id (item.model/create-item! db {:name "foo" :description "bar" :list-id list-id-uuid})
item-id-str (uuid->str item-id)
item-id-uuid (hugsqluuid->javauuid item-id)]
{:item-id item-id
:item-id-str item-id-str
:item-id-uuid item-id-uuid
:list-id-str list-id-str
:list-id-uuid list-id-uuid}))
5 changes: 5 additions & 0 deletions src/listopia/util/db.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(ns listopia.util.db
(:require [hugsql.core :as hugsql]))


(hugsql/def-db-fns "listopia/util/sql/util.sql")
3 changes: 3 additions & 0 deletions src/listopia/util/sql/util.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- src/listopia/util/sql/util.sql
-- listopia utility queries

110 changes: 105 additions & 5 deletions test/listopia/core_test.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,107 @@
(ns listopia.core-test
(:require [clojure.test :refer :all]
[listopia.core :refer :all]))
(:require [clojure.test :refer :all])
(:require [listopia.core :refer :all]
[listopia.db :refer [database-url]]
[listopia.list.model :as list.model]
[listopia.list.handler :as list.handler]
[listopia.item.model :as item.model]
[listopia.item.handler :as item.handler]
[listopia.util.core :as util]
[listopia.util.db :as util.db]))

(deftest a-test
(testing "FIXME, I fail."
(is (= 0 0))))

;; TODO: consider use-fixtures in future


;; create a list
(deftest test-create-list
(testing "list is created"
(is (let [request (util/http-request-mock
:uri "/list/create"
:request-method :post
:params {:name "foo" :description "bar"})]
(= (get (list.handler/handle-create-list! request) :status)
302)))))


(deftest delete-list
(testing "list is deleted"
(is (let [db database-url
test-list (util/generate-test-list db)
list-id-str (get test-list :list-id-str)
list-id-uuid (get test-list :list-id-uuid)
request (util/http-request-mock
:uri (str "/list/delete/" list-id-str)
:request-method :post
:route-params {:list-id list-id-str})
deleted? (= 302 (get (list.handler/handle-delete-list! request) :status))
exists? (nil? (list.model/read-list db {:list-id list-id-uuid}))]
(and deleted? exists?)))))


;; create a list, fetch id, create item with list id
(deftest create-item
(testing "item is created"
(is (let [db database-url
test-list (util/generate-test-list db)
list-id-str (get test-list :list-id-str)
request (util/http-request-mock
:uri "/item/create"
:request-method :post
:params {:name "foo" :description "bar" :list-id list-id-str})]
(= (get (item.handler/handle-create-item! request) :status)
302)))))


;; create a list, fetch id, create item with list id, fetch item id, set checked true
(deftest check-item
(testing "item is marked complete"
(is (let [db database-url
test-item (util/generate-test-item db)
item-id-str (get test-item :item-id-str)
item-id-uuid (get test-item :item-id-uuid)
list-id-str (get test-item :list-id-str)
request (util/http-request-mock
:uri (str "/item/update/" item-id-str)
:request-method :post
:params {:list-id list-id-str :checked "true"}
:route-params {:item-id item-id-str})
updated? (= 302 (get (item.handler/handle-update-item! request) :status))
checked? (get (item.model/read-item db {:item-id item-id-uuid}) :checked)]
(and updated? checked?)))))


;; create a list, fetch id, create item with list id, fetch item id, set checked false
(deftest uncheck-item
(testing "item is marked incomplete"
(is (let [db database-url
test-item (util/generate-test-item db)
item-id-str (get test-item :item-id-str)
item-id-uuid (get test-item :item-id-uuid)
list-id-str (get test-item :list-id-str)
request (util/http-request-mock
:uri (str "/item/update/" item-id-str)
:request-method :post
:params {:list-id list-id-str :checked "false"}
:route-params {:item-id item-id-str})
updated? (= 302 (get (item.handler/handle-update-item! request) :status))
unchecked? (false? (get (item.model/read-item db {:item-id item-id-uuid}) :checked))]
(and updated? unchecked?)))))


;; create a list, fetch id, create item with list id, fetch item id, delete item
(deftest delete-item
(testing "item is deleted"
(is (let [db database-url
test-item (util/generate-test-item db)
item-id-str (get test-item :item-id-str)
item-id-uuid (get test-item :item-id-uuid)
list-id-str (get test-item :list-id-str)
request (util/http-request-mock
:uri (str "/item/delete/" item-id-str)
:request-method :post
:params {:list-id list-id-str}
:route-params {:item-id item-id-str})
deleted? (= 302 (get (item.handler/handle-delete-item! request) :status))
exists? (nil? (item.model/read-item db {:item-id item-id-uuid}))]
(and deleted? exists?)))))

0 comments on commit 0f548ce

Please sign in to comment.