├── README.md
├── store_mock.go
├── bird_handlers.go
├── main.go
├── assets
└── index.html
├── store.go
├── bird_handlers_test.go
├── store_test.go
└── main_test.go
/README.md:
--------------------------------------------------------------------------------
1 | # full-stack-go-
2 | Torun the server on your system:
3 |
4 | Make sure you have dep installed
5 | 1. Run dep ensure to install dependencies
6 | 2. Run go build to create the binary (blog_example__go_web_db)
7 | 3. Run the binary : ./blog_example__go_web_db
8 |
9 | To run tests:
10 | 1. Run dep ensure to install dependencies
11 | 2. Run go test ./...
12 |
13 | Create the birds table before running the application :
14 |
15 | ```
16 | create table birds (
17 | id serial primary key,
18 | species varchar(256),
19 | description varchar(1024)
20 | );
21 | ```
22 |
23 | Before running the application, edit the connString variable inside the main function to specify your postgres database connection
24 |
--------------------------------------------------------------------------------
/store_mock.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/stretchr/testify/mock"
5 | )
6 |
7 | // The mock store contains additonal methods for inspection
8 | type MockStore struct {
9 | mock.Mock
10 | }
11 |
12 | func (m *MockStore) CreateBird(bird *Bird) error {
13 | /*
14 | When this method is called, `m.Called` records the call, and also
15 | returns the result that we pass to it (which you will see in the
16 | handler tests)
17 | */
18 | rets := m.Called(bird)
19 | return rets.Error(0)
20 | }
21 |
22 | func (m *MockStore) GetBirds() ([]*Bird, error) {
23 | rets := m.Called()
24 | /*
25 | Since `rets.Get()` is a generic method, that returns whatever we pass to it,
26 | we need to typecast it to the type we expect, which in this case is []*Bird
27 | */
28 | return rets.Get(0).([]*Bird), rets.Error(1)
29 | }
30 |
31 | func InitMockStore() *MockStore {
32 | /*
33 | Like the InitStore function we defined earlier, this function
34 | also initializes the store variable, but this time, it assigns
35 | a new MockStore instance to it, instead of an actual store
36 | */
37 | s := new(MockStore)
38 | store = s
39 | return s
40 | }
41 |
--------------------------------------------------------------------------------
/bird_handlers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | )
8 |
9 | type Bird struct {
10 | Species string `json:"species"`
11 | Description string `json:"description"`
12 | }
13 |
14 | func getBirdHandler(w http.ResponseWriter, r *http.Request) {
15 | /*
16 | The list of birds is now taken from the store instead of the package level variable we had earlier
17 | */
18 | birds, err := store.GetBirds()
19 |
20 | // Everything else is the same as before
21 | birdListBytes, err := json.Marshal(birds)
22 |
23 | if err != nil {
24 | fmt.Println(fmt.Errorf("Error: %v", err))
25 | w.WriteHeader(http.StatusInternalServerError)
26 | return
27 | }
28 | w.Write(birdListBytes)
29 | }
30 |
31 | func createBirdHandler(w http.ResponseWriter, r *http.Request) {
32 | bird := Bird{}
33 |
34 | err := r.ParseForm()
35 |
36 | if err != nil {
37 | fmt.Println(fmt.Errorf("Error: %v", err))
38 | w.WriteHeader(http.StatusInternalServerError)
39 | return
40 | }
41 |
42 | bird.Species = r.Form.Get("species")
43 | bird.Description = r.Form.Get("description")
44 |
45 | // The only change we made here is to use the `CreateBird` method instead of
46 | // appending to the `bird` variable like we did earlier
47 | err = store.CreateBird(&bird)
48 | if err != nil {
49 | fmt.Println(err)
50 | }
51 |
52 | http.Redirect(w, r, "/assets/", http.StatusFound)
53 | }
54 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/gorilla/mux"
9 | _ "github.com/lib/pq"
10 | )
11 |
12 | // The new router function creates the router and
13 | // returns it to us. We can now use this function
14 | // to instantiate and test the router outside of the main function
15 | func newRouter() *mux.Router {
16 | r := mux.NewRouter()
17 | r.HandleFunc("/hello", handler).Methods("GET")
18 |
19 | // Declare the static file directory and point it to the directory we just made
20 | staticFileDirectory := http.Dir("./assets/")
21 | // Declare the handler, that routes requests to their respective filename.
22 | // The fileserver is wrapped in the `stripPrefix` method, because we want to
23 | // remove the "/assets/" prefix when looking for files.
24 | // For example, if we type "/assets/index.html" in our browser, the file server
25 | // will look for only "index.html" inside the directory declared above.
26 | // If we did not strip the prefix, the file server would look for "./assets/assets/index.html", and yield an error
27 | staticFileHandler := http.StripPrefix("/assets/", http.FileServer(staticFileDirectory))
28 | // The "PathPrefix" method acts as a matcher, and matches all routes starting
29 | // with "/assets/", instead of the absolute route itself
30 | r.PathPrefix("/assets/").Handler(staticFileHandler).Methods("GET")
31 |
32 | r.HandleFunc("/bird", getBirdHandler).Methods("GET")
33 | r.HandleFunc("/bird", createBirdHandler).Methods("POST")
34 | return r
35 | }
36 |
37 | func main() {
38 | fmt.Println("Starting server...")
39 | connString := "dbname=temp sslmode=disable"
40 | db, err := sql.Open("postgres", connString)
41 |
42 | if err != nil {
43 | panic(err)
44 | }
45 | err = db.Ping()
46 |
47 | if err != nil {
48 | panic(err)
49 | }
50 |
51 | InitStore(&dbStore{db: db})
52 |
53 | // The router is now formed by calling the `newRouter` constructor function
54 | // that we defined above. The rest of the code stays the same
55 | r := newRouter()
56 | fmt.Println("Serving on port 8080")
57 | http.ListenAndServe(":8080", r)
58 | }
59 |
60 | func handler(w http.ResponseWriter, r *http.Request) {
61 | fmt.Fprintf(w, "Hello World!")
62 | }
63 |
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | The bird encyclopedia
9 |
10 |
11 |
12 | The bird encyclopedia
13 |
17 |
18 |
19 | | Species |
20 | Description |
21 |
22 | Pigeon |
23 | Common in cities |
24 |
25 |
26 |
27 |
28 |
32 |
40 |
41 |
46 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/store.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // The sql go library is needed to interact with the database
4 | import (
5 | "database/sql"
6 | )
7 |
8 | // Our store will have two methods, to add a new bird,
9 | // and to get all existing birds
10 | // Each method returns an error, in case something goes wrong
11 | type Store interface {
12 | CreateBird(bird *Bird) error
13 | GetBirds() ([]*Bird, error)
14 | }
15 |
16 | // The `dbStore` struct will implement the `Store` interface
17 | // It also takes the sql DB connection object, which represents
18 | // the database connection.
19 | type dbStore struct {
20 | db *sql.DB
21 | }
22 |
23 | func (store *dbStore) CreateBird(bird *Bird) error {
24 | // 'Bird' is a simple struct which has "species" and "description" attributes
25 | // THe first underscore means that we don't care about what's returned from
26 | // this insert query. We just want to know if it was inserted correctly,
27 | // and the error will be populated if it wasn't
28 | _, err := store.db.Query("INSERT INTO birds(species, description) VALUES ($1,$2)", bird.Species, bird.Description)
29 | return err
30 | }
31 |
32 | func (store *dbStore) GetBirds() ([]*Bird, error) {
33 | // Query the database for all birds, and return the result to the
34 | // `rows` object
35 | rows, err := store.db.Query("SELECT species, description from birds")
36 | // We return incase of an error, and defer the closing of the row structure
37 | if err != nil {
38 | return nil, err
39 | }
40 | defer rows.Close()
41 |
42 | // Create the data structure that is returned from the function.
43 | // By default, this will be an empty array of birds
44 | birds := []*Bird{}
45 | for rows.Next() {
46 | // For each row returned by the table, create a pointer to a bird,
47 | bird := &Bird{}
48 | // Populate the `Species` and `Description` attributes of the bird,
49 | // and return incase of an error
50 | if err := rows.Scan(&bird.Species, &bird.Description); err != nil {
51 | return nil, err
52 | }
53 | // Finally, append the result to the returned array, and repeat for
54 | // the next row
55 | birds = append(birds, bird)
56 | }
57 | return birds, nil
58 | }
59 |
60 | // The store variable is a package level variable that will be available for
61 | // use throughout our application code
62 | var store Store
63 |
64 | /*
65 | We will need to call the InitStore method to initialize the store. This will
66 | typically be done at the beginning of our application (in this case, when the server starts up)
67 | This can also be used to set up the store as a mock, which we will be observing
68 | later on
69 | */
70 | func InitStore(s Store) {
71 | store = s
72 | }
73 |
--------------------------------------------------------------------------------
/bird_handlers_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "net/http"
7 | "net/http/httptest"
8 | "net/url"
9 | "strconv"
10 | "testing"
11 | )
12 |
13 | func TestGetBirdsHandler(t *testing.T) {
14 | // Initialize the mock store
15 | mockStore := InitMockStore()
16 |
17 | /* Define the data that we want to return when the mocks `GetBirds` method is
18 | called
19 | Also, we expect it to be called only once
20 | */
21 | mockStore.On("GetBirds").Return([]*Bird{{"sparrow", "A small harmless bird"}}, nil).Once()
22 |
23 | req, err := http.NewRequest("GET", "", nil)
24 |
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 | recorder := httptest.NewRecorder()
29 |
30 | hf := http.HandlerFunc(getBirdHandler)
31 |
32 | // Now, when the handler is called, it should cal our mock store, instead of
33 | // the actual one
34 | hf.ServeHTTP(recorder, req)
35 |
36 | if status := recorder.Code; status != http.StatusOK {
37 | t.Errorf("handler returned wrong status code: got %v want %v",
38 | status, http.StatusOK)
39 | }
40 |
41 | expected := Bird{"sparrow", "A small harmless bird"}
42 | b := []Bird{}
43 | err = json.NewDecoder(recorder.Body).Decode(&b)
44 |
45 | if err != nil {
46 | t.Fatal(err)
47 | }
48 |
49 | actual := b[0]
50 |
51 | if actual != expected {
52 | t.Errorf("handler returned unexpected body: got %v want %v", actual, expected)
53 | }
54 |
55 | // the expectations that we defined in the `On` method are asserted here
56 | mockStore.AssertExpectations(t)
57 | }
58 |
59 | func TestCreateBirdsHandler(t *testing.T) {
60 |
61 | mockStore := InitMockStore()
62 | /*
63 | Similarly, we define our expectations for th `CreateBird` method.
64 | We expect the first argument to the method to be the bird struct
65 | defined below, and tell the mock to return a `nil` error
66 | */
67 | mockStore.On("CreateBird", &Bird{"eagle", "A bird of prey"}).Return(nil)
68 |
69 | form := newCreateBirdForm()
70 | req, err := http.NewRequest("POST", "", bytes.NewBufferString(form.Encode()))
71 |
72 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
73 | req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
74 | if err != nil {
75 | t.Fatal(err)
76 | }
77 | recorder := httptest.NewRecorder()
78 |
79 | hf := http.HandlerFunc(createBirdHandler)
80 |
81 | hf.ServeHTTP(recorder, req)
82 |
83 | if status := recorder.Code; status != http.StatusFound {
84 | t.Errorf("handler returned wrong status code: got %v want %v",
85 | status, http.StatusOK)
86 | }
87 | mockStore.AssertExpectations(t)
88 | }
89 |
90 | func newCreateBirdForm() *url.Values {
91 | form := url.Values{}
92 | form.Set("species", "eagle")
93 | form.Set("description", "A bird of prey")
94 | return &form
95 | }
96 |
--------------------------------------------------------------------------------
/store_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "testing"
6 |
7 | // The "testify/suite" package is used to make the test suite
8 | "github.com/stretchr/testify/suite"
9 | )
10 |
11 | type StoreSuite struct {
12 | suite.Suite
13 | /*
14 | The suite is defined as a struct, with the store and db as its
15 | attributes. Any variables that are to be shared between tests in a
16 | suite should be stored as attributes of the suite instance
17 | */
18 | store *dbStore
19 | db *sql.DB
20 | }
21 |
22 | func (s *StoreSuite) SetupSuite() {
23 | /*
24 | The database connection is opened in the setup, and
25 | stored as an instance variable,
26 | as is the higher level `store`, that wraps the `db`
27 | */
28 | connString := "dbname=temp sslmode=disable"
29 | db, err := sql.Open("postgres", connString)
30 | if err != nil {
31 | s.T().Fatal(err)
32 | }
33 | s.db = db
34 | s.store = &dbStore{db: db}
35 | }
36 |
37 | func (s *StoreSuite) SetupTest() {
38 | /*
39 | We delete all entries from the table before each test runs, to ensure a
40 | consistent state before our tests run. In more complex applications, this
41 | is sometimes achieved in the form of migrations
42 | */
43 | _, err := s.db.Query("DELETE FROM birds")
44 | if err != nil {
45 | s.T().Fatal(err)
46 | }
47 | }
48 |
49 | func (s *StoreSuite) TearDownSuite() {
50 | // Close the connection after all tests in the suite finish
51 | s.db.Close()
52 | }
53 |
54 | // This is the actual "test" as seen by Go, which runs the tests defined below
55 | func TestStoreSuite(t *testing.T) {
56 | s := new(StoreSuite)
57 | suite.Run(t, s)
58 | }
59 |
60 | func (s *StoreSuite) TestCreateBird() {
61 | // Create a bird through the store `CreateBird` method
62 | s.store.CreateBird(&Bird{
63 | Description: "test description",
64 | Species: "test species",
65 | })
66 |
67 | // Query the database for the entry we just created
68 | res, err := s.db.Query(`SELECT COUNT(*) FROM birds WHERE description='test description' AND SPECIES='test species'`)
69 | if err != nil {
70 | s.T().Fatal(err)
71 | }
72 |
73 | // Get the count result
74 | var count int
75 | for res.Next() {
76 | err := res.Scan(&count)
77 | if err != nil {
78 | s.T().Error(err)
79 | }
80 | }
81 |
82 | // Assert that there must be one entry with the properties of the bird that we just inserted (since the database was empty before this)
83 | if count != 1 {
84 | s.T().Errorf("incorrect count, wanted 1, got %d", count)
85 | }
86 | }
87 |
88 | func (s *StoreSuite) TestGetBird() {
89 | // Insert a sample bird into the `birds` table
90 | _, err := s.db.Query(`INSERT INTO birds (species, description) VALUES('bird','description')`)
91 | if err != nil {
92 | s.T().Fatal(err)
93 | }
94 |
95 | // Get the list of birds through the stores `GetBirds` method
96 | birds, err := s.store.GetBirds()
97 | if err != nil {
98 | s.T().Fatal(err)
99 | }
100 |
101 | // Assert that the count of birds received must be 1
102 | nBirds := len(birds)
103 | if nBirds != 1 {
104 | s.T().Errorf("incorrect count, wanted 1, got %d", nBirds)
105 | }
106 |
107 | // Assert that the details of the bird is the same as the one we inserted
108 | expectedBird := Bird{"bird", "description"}
109 | if *birds[0] != expectedBird {
110 | s.T().Errorf("incorrect details, expected %v, got %v", expectedBird, *birds[0])
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io/ioutil"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 | )
9 |
10 | func TestHandler(t *testing.T) {
11 | //Here, we form a new HTTP request. This is the request that's going to be passed to our handler.
12 | // The first argument is the method, the second argument is the route, and the third is the request body, which we don't have in this case.
13 | req, err := http.NewRequest("GET", "", nil)
14 |
15 | // In case there is an error in forming the request, we fail and stop the test
16 | if err != nil {
17 | t.Fatal(err)
18 | }
19 |
20 | // We use Go's httptest library to create an http recorder. This recorder will act as the target of our http request
21 | // (you can think of it as a mini-browser, which will accept the result of the http request that we make)
22 | recorder := httptest.NewRecorder()
23 |
24 | // Create an HTTP handler from our handler function. "handler" is the handler function defined in our main.go file that we want to test
25 | hf := http.HandlerFunc(handler)
26 |
27 | // Serve the HTTP request to our recorder. This is the line that actually executes our the handler that we want to test
28 | hf.ServeHTTP(recorder, req)
29 |
30 | // Check the status code is what we expect.
31 | if status := recorder.Code; status != http.StatusOK {
32 | t.Errorf("handler returned wrong status code: got %v want %v",
33 | status, http.StatusOK)
34 | }
35 |
36 | // Check the response body is what we expect.
37 | expected := `Hello World!`
38 | actual := recorder.Body.String()
39 | if actual != expected {
40 | t.Errorf("handler returned unexpected body: got %v want %v", actual, expected)
41 | }
42 | }
43 |
44 | func TestRouter(t *testing.T) {
45 | // Instantiate the router using the constructor function that
46 | // we defined previously
47 | r := newRouter()
48 |
49 | // Create a new server using the "httptest" libraries `NewServer` method
50 | // Documentation : https://golang.org/pkg/net/http/httptest/#NewServer
51 | mockServer := httptest.NewServer(r)
52 |
53 | // The mock server we created runs a server and exposes its location in the URL attribute
54 | // We make a GET request to the "hello" route we defined in the router
55 | resp, err := http.Get(mockServer.URL + "/hello")
56 |
57 | // Handle any unexpected error
58 | if err != nil {
59 | t.Fatal(err)
60 | }
61 |
62 | // We want our status to be 200 (ok)
63 | if resp.StatusCode != http.StatusOK {
64 | t.Errorf("Status should be ok, got %d", resp.StatusCode)
65 | }
66 |
67 | // In the next few lines, the response body is read, and converted to a string
68 | defer resp.Body.Close()
69 | // read the body into a bunch of bytes (b)
70 | b, err := ioutil.ReadAll(resp.Body)
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 | // convert the bytes to a string
75 | respString := string(b)
76 | expected := "Hello World!"
77 |
78 | // We want our response to match the one defined in our handler.
79 | // If it does happen to be "Hello world!", then it confirms, that the
80 | // route is correct
81 | if respString != expected {
82 | t.Errorf("Response should be %s, got %s", expected, respString)
83 | }
84 |
85 | }
86 |
87 | func TestRouterForNonExistentRoute(t *testing.T) {
88 | r := newRouter()
89 | mockServer := httptest.NewServer(r)
90 | // Most of the code is similar. The only difference is that now we make a //request to a route we know we didn't define, like the `PUT /hello` route.
91 | resp, err := http.Post(mockServer.URL+"/hello", "", nil)
92 |
93 | if err != nil {
94 | t.Fatal(err)
95 | }
96 |
97 | // We want our status to be 405 (method not allowed)
98 | if resp.StatusCode != http.StatusMethodNotAllowed {
99 | t.Errorf("Status should be 405, got %d", resp.StatusCode)
100 | }
101 |
102 | // The code to test the body is also mostly the same, except this time, we need an empty body
103 | defer resp.Body.Close()
104 | b, err := ioutil.ReadAll(resp.Body)
105 | if err != nil {
106 | t.Fatal(err)
107 | }
108 | respString := string(b)
109 | expected := ""
110 |
111 | if respString != expected {
112 | t.Errorf("Response should be %s, got %s", expected, respString)
113 | }
114 |
115 | }
116 |
117 | func TestStaticFileServer(t *testing.T) {
118 | r := newRouter()
119 | mockServer := httptest.NewServer(r)
120 |
121 | // We want to hit the `GET /assets/` route to get the index.html file response
122 | resp, err := http.Get(mockServer.URL + "/assets/")
123 |
124 | if err != nil {
125 | t.Fatal(err)
126 | }
127 |
128 | // We want our status to be 200 (ok)
129 | if resp.StatusCode != http.StatusOK {
130 | t.Errorf("Status should be 200, got %d", resp.StatusCode)
131 | }
132 |
133 | // It isn't wise to test the entire content of the HTML file.
134 | // Instead, we test that the content-type header is "text/html; charset=utf-8"
135 | // so that we know that an html file has been served
136 | contentType := resp.Header.Get("Content-Type")
137 | expectedContentType := "text/html; charset=utf-8"
138 |
139 | if expectedContentType != contentType {
140 | t.Errorf("Wrong content type, expected %s, got %s", expectedContentType, contentType)
141 | }
142 |
143 | }
144 |
--------------------------------------------------------------------------------