├── 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 | 20 | 21 | 22 | 23 | 24 | 25 |
SpeciesDescription
PigeonCommon in cities
26 |
27 | 28 | 32 |
33 | Species: 34 | 35 |
Description: 36 | 37 |
38 | 39 |
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 | --------------------------------------------------------------------------------