Это продолжение материала, часть 1.
Выводим список топиков
Сейчас мы добавим функционал для показа списка всех топиков на главной странице.
Настройка роута
В предыдущем разделе мы создали роут и его описание в файле main.go. С ростом размера приложения будет лучше перенести описания роутов в отдельный файл. Мы создадим функцию initializeRoutes() в файле routes.go и будем вызывать её из функции main() для настройки всех роутов. Вместо создания линейного обработчика роутов, мы сделаем роуты отдельными функциями.
После всего этого файл routes.go будет таким:
// routes.go
package main
func initializeRoutes() {
// определение роута главной страницы
router.GET("/", showIndexPage)
}
Так как мы выводим список топиков на главной странице, нам не нужно будет создавать больше никаких других роутов.
Файл main.go должен быть сейчас таким:
// main.go
package main
import "github.com/gin-gonic/gin"
var router *gin.Engine
func main() {
// роутер по-умолчанию в Gin
router = gin.Default()
// Обработаем шаблоны вначале, так что их не нужно будет перечитывать
// ещё раз. Из-за этого вывод HTML-страниц такой быстрый.
router.LoadHTMLGlob("templates/*")
// Инициализируем роуты
initializeRoutes()
// Запускаем приложение
router.Run()
}
Проектирование модели топика
Сделаем структуру топика простой, всего с тремя полями — Id, Title (название) и Content (содержание). Её можно описать так:
type article struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
Большинство приложений используют базу данных для хранения данных. Чтобы не усложнять, мы будем хранить список топиков в памяти и заполнять его при создании двумя следующими топиками:
var articleList = []article{
article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}
Мы вставим этот кусок кода в новый файл models.article.go. Сейчас нам понадобится функция, возвращающая список всех топиков. Мы её назовём getAllArticles() и положим её в этот же файл. И создадим тест для неё. Мы назовём этот тест TestGetAllArticles и вставим его в файл models.article_test.go.
Создадим тест (TestGetAllArticles) для функции getAllArticles(). В результате файл models.article_test.go будет таким:
// models.article_test.go
package main
import "testing"
// Test the function that fetches all articles
func TestGetAllArticles(t *testing.T) {
alist := getAllArticles()
// Check that the length of the list of articles returned is the
// same as the length of the global variable holding the list
if len(alist) != len(articleList) {
t.Fail()
}
// Check that each member is identical
for i, v := range alist {
if v.Content != articleList[i].Content ||
v.ID != articleList[i].ID ||
v.Title != articleList[i].Title {
t.Fail()
break
}
}
}
В этом тесте используется функция getAllArticles() для получения списка всех топиков. Сперва этот тест проверяет, что эта функция получает список топиков и этот список идентичен списку, загруженному в глобальную переменную articleList. Затем он проходит в цикле по списку топиков для проверки уникальности каждого. Если хотя бы одна из этих проверок не удалась, тест возвращает неудачу.
После написания теста приступим к написанию кода модуля. Файл models.article.go будет содержать такой код:
// models.article.go
package main
type article struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
// For this demo, we're storing the article list in memory
// In a real application, this list will most likely be fetched
// from a database or from static files
var articleList = []article{
article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}
// Return a list of all the articles
func getAllArticles() []article {
return articleList
}
Создание шаблона представления
Так как список топиков будет выводится на главной странице, нам не нужно создавать новый шаблон. Однако, нам нужно изменить шаблон index.html для вывода в него списка топиков.
Предположим, что список топиков будет передан в шаблон в переменной payload. Тогда следующий снипет выведет список всех топиков:
{{range .payload }}
<!--Create the link for the article based on its ID-->
<a href="/article/view/{{.ID}}">
<!--Display the title of the article -->
<h2>{{.Title}}</h2>
</a>
<!--Display the content of the article-->
<p>{{.Content}}</p>
{{end}}
Этот снипет пройдётся по всем элементам переменной payload и выведет название и текст каждого топика. Также этот снипет добавит ссылку в каждый топик. Однако, пока мы ещё не создали обработчик соответствующего роута, и эти ссылки на топики не будут работать.
Обновлённый index.html будет таким:
<!--index.html-->
<!--Embed the header.html template at this location-->
{{ template "header.html" .}}
<!--Loop over the `payload` variable, which is the list of articles-->
{{range .payload }}
<!--Create the link for the article based on its ID-->
<a href="/article/view/{{.ID}}">
<!--Display the title of the article -->
<h2>{{.Title}}</h2>
</a>
<!--Display the content of the article-->
<p>{{.Content}}</p>
{{end}}
<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}
Определяем требования к обработчику роута с помощью юнит-теста
Перед созданием обработчика роута главной страницы, мы создадим тест, чтобы определить поведение этого обработчика. Этот тест проверит следующие условия:
- Обработчик отвечает статус-кодом HTTP 200,
- Возвращаемый HTML содержит тег title с текстом «Home Page».
Код теста поместим в функцию TestShowIndexPageUnauthenticated в файл handlers.article_test.go. Вспомогательные функции, используемые в этом тесте, мы разместим в файле common_test.go.
Вот содержимое файла handlers.article_test.go:
// handlers.article_test.go
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// Test that a GET request to the home page returns the home page with
// the HTTP code 200 for an unauthenticated user
func TestShowIndexPageUnauthenticated(t *testing.T) {
r := getRouter(true)
r.GET("/", showIndexPage)
// Create a request to send to the above route
req, _ := http.NewRequest("GET", "/", nil)
testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
// Test that the http status code is 200
statusOK := w.Code == http.StatusOK
// Test that the page title is "Home Page"
// You can carry out a lot more detailed tests using libraries that can
// parse and process HTML pages
p, err := ioutil.ReadAll(w.Body)
pageOK := err == nil && strings.Index(string(p), "<title>Home Page</title>") > 0
return statusOK && pageOK
})
}
Файл common_test.go:
package main
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/gin-gonic/gin"
)
var tmpArticleList []article
// This function is used for setup before executing the test functions
func TestMain(m *testing.M) {
//Set Gin to Test Mode
gin.SetMode(gin.TestMode)
// Run the other tests
os.Exit(m.Run())
}
// Helper function to create a router during testing
func getRouter(withTemplates bool) *gin.Engine {
r := gin.Default()
if withTemplates {
r.LoadHTMLGlob("templates/*")
}
return r
}
// Helper function to process a request and test its response
func testHTTPResponse(t *testing.T, r *gin.Engine, req *http.Request, f func(w *httptest.ResponseRecorder) bool) {
// Create a response recorder
w := httptest.NewRecorder()
// Create the service and process the above request.
r.ServeHTTP(w, req)
if !f(w) {
t.Fail()
}
}
// This function is used to store the main lists into the temporary one
// for testing
func saveLists() {
tmpArticleList = articleList
}
// This function is used to restore the main lists from the temporary one
func restoreLists() {
articleList = tmpArticleList
}
Для написания теста мы написали несколько вспомогательных функций. Они в дальнейшем помогут нам уменьшить количество кода при написании похожих тестов.
Функция TestMain переводит Gin в тестовый режим и вызывает функции тестирования. Функция getRouter создаёт и возвращает роутер. Функция saveLists() помещает список топиков во временную переменную. Она используется в функции restoreLists() для восстановления списка топиков до первоначального состояния после выполнения юнит-теста.
И, наконец, функция testHTTPResponse выполняет переданную ей функцию для проверки — возвращает ли она булево значение true — показывая успешность теста, или нет. Эта функция помогает нам избежать дублирования кода, необходимого для тестирования ответа на HTTP-запрос.
Для проверки HTTP-кода и возвращаемого HTML, сделаем следующее:
- Создадим новый роутер,
- Определим роуту тот же обработчик, что используется в главном приложении (showIndexPage),
- Создадим новый запрос для доступа к роуту,
- Создадим функцию, обрабатывающую ответ, для тестирования HTTP-кода и HTML, и
- Вызовем testHTTPResponse() из новой функции для завершения теста.
Создание обработчика роута
Мы будет создавать все обработчики роутов, относящихся к топикам, в файле handlers.article.go. Обработчик главной страницы, showIndexPage выполняет следующие задачи:
1. Получает список топиков
Это делается с помощью функции getAllArticles, созданной ранее:
articles := getAllArticles()
2. Обрабатывает шаблон index.html, передавая ему список топиков
Это делается с помощью кода ниже:
c.HTML(
// Set the HTTP status to 200 (OK)
http.StatusOK,
// Use the index.html template
"index.html",
// Pass the data that the page uses
gin.H{
"title": "Home Page",
"payload": articles,
},
)
Разница с кодом из предыдущего раздела только в том, что мы передаём список топиков, который можно прочитать в шаблоне в переменной payload.
Файл handlers.article.go должен быть таким:
// handlers.article.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func showIndexPage(c *gin.Context) {
articles := getAllArticles()
// Call the HTML method of the Context to render a template
c.HTML(
// Set the HTTP status to 200 (OK)
http.StatusOK,
// Use the index.html template
"index.html",
// Pass the data that the page uses
gin.H{
"title": "Home Page",
"payload": articles,
},
)
}
Если сейчас собрать и запустить приложение, открыть в браузере http://localhost:8080, оно будет выглядеть так:
Новые файлы, добавленные в этом разделе:
├── common_test.go
├── handlers.article.go
├── handlers.article_test.go
├── models.article.go
├── models.article_test.go
└── routes.go
Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.
Пишите: @ighar. Buy me a coffee, please :).