Skip to content

Commit 15bc873

Browse files
initial commit
0 parents  commit 15bc873

File tree

10 files changed

+536
-0
lines changed

10 files changed

+536
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.DS_Store

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# testify-tutorial
2+
3+
This repository contains the code for my testify tutorial [youtube video series](https://www.youtube.com/watch?v=Su6zn1_blw0&ab_channel=ThomasLanghorst). It shows how to use [testify](https://github.com/stretchr/testify) together with [mockery](https://github.com/mockery/mockery) to build awesome golang tests.
4+
5+
## Description
6+
7+
The tests are written for the `calculations` package and test the [PriceIncreaseCalculator](./calculations/priceIncrease.go) functionality which depends on [PriceProvider](./stocks/stocks.go) interface.
8+
9+
## Prepare Postgres in Docker
10+
If you want to run integration tests you need to have a running postgres instance. The constants that are being used are the default ones from [dockerhub](https://hub.docker.com/_/postgres)
11+
12+
```go
13+
const (
14+
dbHost = "localhost"
15+
dbPort = 5432
16+
dbUser = "postgres"
17+
dbPassword = "mysecretpassword"
18+
dbName = "postgres"
19+
)
20+
```
21+
22+
All you need to do is execute the following command and you should be good to go
23+
24+
```
25+
docker run --name pg-testify -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 postgres
26+
```
27+
28+
## Running the tests
29+
30+
```go
31+
# run all tests
32+
go test ./calculations
33+
34+
# only run unit tests
35+
go test ./calculations -run UnitTestSuite -v
36+
37+
# only run integration tests
38+
# running postgres instance required
39+
go test ./calculations -run IntTestSuite -v
40+
````
41+

calculations/priceIncrease.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package calculations
2+
3+
import (
4+
"errors"
5+
"testify-tutorial/stocks"
6+
"time"
7+
)
8+
9+
type PriceIncreaseCalculator interface {
10+
PriceIncrease() (float64, error)
11+
}
12+
13+
type priceIncreaseCalculator struct {
14+
PriceProvider stocks.PriceProvider
15+
}
16+
17+
func NewPriceIncreaseCalculator(pp stocks.PriceProvider) PriceIncreaseCalculator {
18+
return &priceIncreaseCalculator{
19+
PriceProvider: pp,
20+
}
21+
}
22+
23+
func (pic *priceIncreaseCalculator) PriceIncrease() (float64, error) {
24+
25+
prices, err := pic.PriceProvider.List(time.Now())
26+
if err != nil {
27+
return 0.0, err
28+
}
29+
30+
if len(prices) < 2 {
31+
return 0.0, errors.New("not enough data")
32+
}
33+
34+
return (prices[0].Price/prices[1].Price - 1.0) * 100.0, nil
35+
}

calculations/priceIncrease_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package calculations
2+
3+
import (
4+
"database/sql"
5+
"errors"
6+
"fmt"
7+
"testify-tutorial/mocks"
8+
"testify-tutorial/stocks"
9+
"testing"
10+
"time"
11+
12+
_ "github.com/lib/pq"
13+
"github.com/stretchr/testify/mock"
14+
"github.com/stretchr/testify/suite"
15+
)
16+
17+
const (
18+
dbHost = "localhost"
19+
dbPort = 5432
20+
dbUser = "postgres"
21+
dbPassword = "mysecretpassword"
22+
dbName = "postgres"
23+
)
24+
25+
// Integration tests
26+
27+
type IntTestSuite struct {
28+
suite.Suite
29+
db *sql.DB
30+
calculator PriceIncreaseCalculator
31+
}
32+
33+
func TestIntTestSuite(t *testing.T) {
34+
suite.Run(t, &IntTestSuite{})
35+
}
36+
37+
func (its *IntTestSuite) SetupSuite() {
38+
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", dbHost, dbPort, dbUser, dbPassword, dbName)
39+
db, err := sql.Open("postgres", psqlInfo)
40+
if err != nil {
41+
its.FailNowf("unable to connect to database", err.Error())
42+
}
43+
44+
setupDatabase(its, db)
45+
46+
pp := stocks.NewPriceProvider(db)
47+
calculator := NewPriceIncreaseCalculator(pp)
48+
49+
its.db = db
50+
its.calculator = calculator
51+
}
52+
53+
func (its *IntTestSuite) BeforeTest(suiteName, testName string) {
54+
if testName == "TestCalculate_Error" {
55+
return
56+
}
57+
seedTestTable(its, its.db) // ts -> price=1, ts+1min -> price=2
58+
}
59+
60+
func (its *IntTestSuite) TearDownSuite() {
61+
tearDownDatabase(its)
62+
}
63+
64+
func (its *IntTestSuite) TearDownTest() {
65+
cleanTable(its)
66+
}
67+
68+
func (its *IntTestSuite) TestCalculate_Error() {
69+
70+
actual, err := its.calculator.PriceIncrease()
71+
72+
its.EqualError(err, "not enough data")
73+
its.Equal(0.0, actual)
74+
75+
}
76+
77+
func (its *IntTestSuite) TestCalculate() {
78+
79+
actual, err := its.calculator.PriceIncrease()
80+
81+
its.Nil(err)
82+
its.Equal(100.0, actual)
83+
84+
}
85+
86+
// Helper functions
87+
88+
func setupDatabase(its *IntTestSuite, db *sql.DB) {
89+
its.T().Log("setting up database")
90+
91+
_, err := db.Exec(`CREATE DATABASE stockprices_test`)
92+
if err != nil {
93+
its.FailNowf("unable to create database", err.Error())
94+
}
95+
96+
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS stockprices (
97+
timestamp TIMESTAMPTZ PRIMARY KEY,
98+
price DECIMAL NOT NULL
99+
)`)
100+
101+
if err != nil {
102+
its.FailNowf("unable to create table", err.Error())
103+
}
104+
105+
}
106+
107+
func seedTestTable(its *IntTestSuite, db *sql.DB) {
108+
its.T().Log("seeding test table")
109+
110+
for i := 1; i <= 2; i++ {
111+
_, err := db.Exec("INSERT INTO stockprices (timestamp, price) VALUES ($1,$2)", time.Now().Add(time.Duration(i)*time.Minute), float64(i))
112+
if err != nil {
113+
its.FailNowf("unable to seed table", err.Error())
114+
}
115+
}
116+
}
117+
118+
func cleanTable(its *IntTestSuite) {
119+
its.T().Log("cleaning database")
120+
121+
_, err := its.db.Exec(`DELETE FROM stockprices`)
122+
if err != nil {
123+
its.FailNowf("unable to clean table", err.Error())
124+
}
125+
}
126+
127+
func tearDownDatabase(its *IntTestSuite) {
128+
its.T().Log("tearing down database")
129+
130+
_, err := its.db.Exec(`DROP TABLE stockprices`)
131+
if err != nil {
132+
its.FailNowf("unable to drop table", err.Error())
133+
}
134+
135+
_, err = its.db.Exec(`DROP DATABASE stockprices_test`)
136+
if err != nil {
137+
its.FailNowf("unable to drop database", err.Error())
138+
}
139+
140+
err = its.db.Close()
141+
if err != nil {
142+
its.FailNowf("unable to close database", err.Error())
143+
}
144+
}
145+
146+
// Unit tests
147+
148+
type UnitTestSuite struct {
149+
suite.Suite
150+
calculator PriceIncreaseCalculator
151+
priceProviderMock *mocks.PriceProvider
152+
}
153+
154+
func TestUnitTestSuite(t *testing.T) {
155+
suite.Run(t, &UnitTestSuite{})
156+
}
157+
158+
func (uts *UnitTestSuite) SetupTest() {
159+
priceProviderMock := mocks.PriceProvider{}
160+
calculator := NewPriceIncreaseCalculator(&priceProviderMock)
161+
162+
uts.calculator = calculator
163+
uts.priceProviderMock = &priceProviderMock
164+
}
165+
166+
func (uts *UnitTestSuite) TestCalculate() {
167+
uts.priceProviderMock.On("List", mock.Anything).Return([]*stocks.PriceData{}, nil)
168+
169+
actual, err := uts.calculator.PriceIncrease()
170+
171+
uts.Equal(0.0, actual)
172+
uts.EqualError(err, "not enough data")
173+
}
174+
175+
func (uts *UnitTestSuite) TestCalculate_ErrorFromPriceProvider() {
176+
expectedError := errors.New("oh my god")
177+
178+
uts.priceProviderMock.On("List", mock.Anything).Return([]*stocks.PriceData{}, expectedError)
179+
180+
actual, err := uts.calculator.PriceIncrease()
181+
182+
uts.Equal(0.0, actual)
183+
uts.Equal(expectedError, err)
184+
185+
}

go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module testify-tutorial
2+
3+
go 1.17
4+
5+
require (
6+
github.com/lib/pq v1.10.4
7+
github.com/sirupsen/logrus v1.8.1
8+
github.com/stretchr/testify v1.7.1
9+
)
10+
11+
require (
12+
github.com/davecgh/go-spew v1.1.1 // indirect
13+
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
github.com/stretchr/objx v0.1.0 // indirect
15+
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
16+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
17+
)

go.sum

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
5+
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
6+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8+
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
9+
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
10+
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
11+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
12+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
13+
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
14+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
15+
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
16+
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
17+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
18+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
19+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
20+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"testify-tutorial/calculations"
7+
"testify-tutorial/stocks"
8+
"time"
9+
10+
_ "github.com/lib/pq"
11+
log "github.com/sirupsen/logrus"
12+
)
13+
14+
const (
15+
dbHost = "localhost"
16+
dbPort = 5432
17+
dbUser = "postgres"
18+
dbPassword = "mysecretpassword"
19+
dbName = "postgres"
20+
)
21+
22+
func main() {
23+
24+
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", dbHost, dbPort, dbUser, dbPassword, dbName)
25+
db, err := sql.Open("postgres", psqlInfo)
26+
if err != nil {
27+
panic(err)
28+
}
29+
30+
createTable(db)
31+
seedTable(db) // needs only be executed once
32+
33+
pp := stocks.NewPriceProvider(db)
34+
calculator := calculations.NewPriceIncreaseCalculator(pp)
35+
36+
increase, err := calculator.PriceIncrease()
37+
if err != nil {
38+
panic(err)
39+
}
40+
41+
fmt.Println(increase)
42+
}
43+
44+
func createTable(db *sql.DB) {
45+
stmt, err := db.Prepare(`CREATE TABLE IF NOT EXISTS stockprices (
46+
timestamp TIMESTAMPTZ PRIMARY KEY,
47+
price DECIMAL NOT NULL
48+
)`)
49+
50+
if err != nil {
51+
log.Fatalf("unable to prepare create table statement. Error %s", err.Error())
52+
}
53+
54+
_, err = stmt.Exec()
55+
if err != nil {
56+
log.Fatalf("unable to execute create table statement. Error %s", err.Error())
57+
}
58+
}
59+
60+
func seedTable(db *sql.DB) {
61+
log.Info("Seeding stockprices table")
62+
63+
for i := 1; i <= 5; i++ {
64+
_, err := db.Exec("INSERT INTO stockprices (timestamp, price) VALUES ($1,$2)", time.Now().Add(time.Duration(-i)*time.Minute), float64((6-i)*5))
65+
if err != nil {
66+
log.Fatalf("unable to insert into stockprices table. Error %s", err.Error())
67+
}
68+
}
69+
70+
}

0 commit comments

Comments
 (0)