├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docker-compose.yml
├── mysql
├── my.cnf
└── setup.sql
└── server
├── Gopkg.lock
├── Gopkg.toml
├── Makefile
├── adapter
└── api
│ ├── api_helper.go
│ ├── api_helper_test.go
│ ├── const.go
│ ├── err.go
│ ├── program_lang_api.go
│ └── program_lang_api_test.go
├── domain
├── model
│ ├── const.go
│ ├── error.go
│ ├── programming_lang.go
│ └── test_helper.go
├── repository
│ └── programming_lang_repository.go
└── service
│ ├── program_lang_service.go
│ └── program_lang_service_test.go
├── infra
├── dao
│ ├── mock
│ │ └── programming_lang_repository_mock.go
│ └── rdb
│ │ ├── const.go
│ │ ├── program_lang_dao.go
│ │ ├── program_lang_dao_test.go
│ │ ├── sql_manager.go
│ │ └── sql_manager_interface.go
└── router
│ └── router.go
├── main.go
├── usecase
├── input
│ └── programming_lang_input.go
├── mock
│ └── programming_lang_input_mock.go
├── program_lang_usecase.go
└── program_lang_usecase_test.go
└── util
├── checker.go
├── checker_test.go
├── trimer.go
└── trimer_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### macOS template
3 | # General
4 | .DS_Store
5 | .AppleDouble
6 | .LSOverride
7 |
8 | # Icon must end with two \r
9 | Icon
10 |
11 | # Thumbnails
12 | ._*
13 |
14 | # Files that might appear in the root of a volume
15 | .DocumentRevisions-V100
16 | .fseventsd
17 | .Spotlight-V100
18 | .TemporaryItems
19 | .Trashes
20 | .VolumeIcon.icns
21 | .com.apple.timemachine.donotpresent
22 |
23 | # Directories potentially created on remote AFP share
24 | .AppleDB
25 | .AppleDesktop
26 | Network Trash Folder
27 | Temporary Items
28 | .apdisk
29 | ### Go template
30 | # Binaries for programs and plugins
31 | *.exe
32 | *.exe~
33 | *.dll
34 | *.so
35 | *.dylib
36 |
37 | # Test binary, build with `go test -c`
38 | *.test
39 |
40 | # Output of the go coverage tool, specifically when used with LiteIDE
41 | *.out
42 |
43 | .idea/
44 | server/vendor/
45 | server/gobinary
46 | mysql/data/
47 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | 0.9.9 : Release Product without auto DB config.
2 | 0.9.9.1 : modify README.md
3 | 1.0.0 : modify bug
4 | 1.0.1 : modify README.md
5 | 1.0.2 : modify repository test
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 sekky0905
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # clean-architecture-with-go
2 |
3 | The sample codes of Clean Architecture with Go.
4 |
5 | ## How to start
6 | 1 clone this repository.
7 |
8 | ```
9 | cd ${Your_Working_Directory(Working Directory should be on your $GOPATH/src)}
10 | git clone git@github.com:SekiguchiKai/clean-architecture-with-go.git
11 | ```
12 |
13 | 2 initialize the project by Makefile.
14 |
15 | ```
16 | cd clean-architecture-with-go/server
17 | make init
18 | ```
19 |
20 | 3 start application.
21 |
22 | ```
23 | docker-compose up -d
24 | ```
25 |
26 | ### Access Point
27 |
28 | #### LIST
29 | ```
30 | http://localhost:8080/v1/langs?limit=${num}
31 | ```
32 |
33 | #### GET
34 | ```
35 | http://localhost:8080/v1/langs/${id}
36 | ```
37 |
38 | #### POST
39 | ```
40 | http://localhost:8080/v1/langs
41 | ```
42 |
43 | #### PUT
44 | ```
45 | http://localhost:8080/v1/langs/${id}
46 | ```
47 |
48 | #### DELETE
49 | ```
50 | http://localhost:8080/v1/langs/${id}
51 | ```
52 |
53 | #### Json Data Format sample
54 |
55 | You can Post or Put Data like the sample below.
56 |
57 | ```
58 | {
59 | "name":"JavaScript",
60 | "feature":"Dynamic type. Works on Web browser."
61 | }
62 | ```
63 |
64 | ## Reference
65 |
66 | エリック・エヴァンス(著)、 今関 剛 (監修)、 和智 右桂 (翻訳) (2011/4/9)『エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)』 翔泳社
67 |
68 | Robert C.Martin (著)、 角 征典 (翻訳)、 高木 正弘 (翻訳) (2018/7/27)『Clean Architecture 達人に学ぶソフトウェアの構造と設計』 KADOKAWA
69 |
70 | アラン・シャロウェイ (著)、 ジェームズ・R・トロット (著)、 村上 雅章 (翻訳) (2014/3/11)『オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)』 丸善出版
71 |
72 | 結城 浩 (2004/6/19)『増補改訂版Java言語で学ぶデザインパターン入門』 ソフトバンククリエイティブ
73 |
74 | InfoQ.com、徳武 聡(翻訳) (2009年6月7日) 『Domain Driven Design(ドメイン駆動設計) Quickly 日本語版』 InfoQ.com Domain Driven Design(ドメイン駆動設計) Quickly 日本語版
75 |
76 | https://blog.tai2.net/the_clean_architecture.html
77 |
78 | https://nrslib.com/clean-architecture/
79 |
80 | https://www.slideshare.net/pospome/go-80591000
81 |
82 | https://qiita.com/hirotakan/items/698c1f5773a3cca6193e
83 |
84 | https://postd.cc/golang-clean-archithecture/
85 |
86 | https://qiita.com/kondei/items/41c28674c1bfd4156186
87 |
88 | https://golang.org/pkg/database/sql
89 |
90 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | db:
5 | image: mysql:5.7
6 | environment:
7 | MYSQL_DATABASE: "sample"
8 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
9 | volumes:
10 | - "./mysql:/etc/mysql/conf.d"
11 | - "./mysql/data:/var/lib/mysql"
12 | - "./mysql:/docker-entrypoint-initdb.d"
13 | ports:
14 | - "3306:3306"
15 | app:
16 | image: golang:1.9
17 | command: "go run main.go"
18 | volumes:
19 | - ./server:/go/src/github.com/SekiguchiKai/clean-architecture-with-go/server
20 | working_dir: /go/src/github.com/SekiguchiKai/clean-architecture-with-go/server
21 | ports:
22 | - "8080:8080"
23 | depends_on:
24 | - db
25 |
--------------------------------------------------------------------------------
/mysql/my.cnf:
--------------------------------------------------------------------------------
1 | [mysqld]
2 | character-set-server=utf8mb4
3 | collation-server=utf8mb4_general_ci
4 |
5 |
6 | [client]
7 | default-character-set=utf8mb4
8 |
--------------------------------------------------------------------------------
/mysql/setup.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE programming_langs (
2 | id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
3 | name VARCHAR(20) NOT NULL,
4 | feature TEXT,
5 | created_at datetime DEFAULT NULL,
6 | updated_at datetime DEFAULT NULL,
7 | PRIMARY KEY (id)
8 | );
9 |
10 | ALTER DATABASE sample CHARACTER SET utf8mb4;
11 | ALTER TABLE programming_langs CONVERT TO CHARACTER SET utf8mb4;
12 |
13 |
--------------------------------------------------------------------------------
/server/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | branch = "master"
6 | name = "github.com/gin-contrib/sse"
7 | packages = ["."]
8 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
9 |
10 | [[projects]]
11 | name = "github.com/gin-gonic/gin"
12 | packages = [
13 | ".",
14 | "binding",
15 | "json",
16 | "render"
17 | ]
18 | revision = "b869fe1415e4b9eb52f247441830d502aece2d4d"
19 | version = "v1.3.0"
20 |
21 | [[projects]]
22 | name = "github.com/go-sql-driver/mysql"
23 | packages = ["."]
24 | revision = "d523deb1b23d913de5bdada721a6071e71283618"
25 | version = "v1.4.0"
26 |
27 | [[projects]]
28 | name = "github.com/golang/mock"
29 | packages = ["gomock"]
30 | revision = "c34cdb4725f4c3844d095133c6e40e448b86589b"
31 | version = "v1.1.1"
32 |
33 | [[projects]]
34 | name = "github.com/golang/protobuf"
35 | packages = ["proto"]
36 | revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
37 | version = "v1.2.0"
38 |
39 | [[projects]]
40 | name = "github.com/json-iterator/go"
41 | packages = ["."]
42 | revision = "1624edc4454b8682399def8740d46db5e4362ba4"
43 | version = "v1.1.5"
44 |
45 | [[projects]]
46 | name = "github.com/mattn/go-isatty"
47 | packages = ["."]
48 | revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
49 | version = "v0.0.4"
50 |
51 | [[projects]]
52 | name = "github.com/modern-go/concurrent"
53 | packages = ["."]
54 | revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
55 | version = "1.0.3"
56 |
57 | [[projects]]
58 | name = "github.com/modern-go/reflect2"
59 | packages = ["."]
60 | revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
61 | version = "1.0.1"
62 |
63 | [[projects]]
64 | name = "github.com/pkg/errors"
65 | packages = ["."]
66 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
67 | version = "v0.8.0"
68 |
69 | [[projects]]
70 | name = "github.com/ugorji/go"
71 | packages = ["codec"]
72 | revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
73 | version = "v1.1.1"
74 |
75 | [[projects]]
76 | branch = "master"
77 | name = "golang.org/x/net"
78 | packages = ["context"]
79 | revision = "26e67e76b6c3f6ce91f7c52def5af501b4e0f3a2"
80 |
81 | [[projects]]
82 | branch = "master"
83 | name = "golang.org/x/sys"
84 | packages = ["unix"]
85 | revision = "1561086e645b2809fb9f8a1e2a38160bf8d53bf4"
86 |
87 | [[projects]]
88 | name = "google.golang.org/appengine"
89 | packages = ["cloudsql"]
90 | revision = "ae0ab99deb4dc413a2b4bd6c8bdd0eb67f1e4d06"
91 | version = "v1.2.0"
92 |
93 | [[projects]]
94 | name = "gopkg.in/DATA-DOG/go-sqlmock.v1"
95 | packages = ["."]
96 | revision = "d76b18b42f285b792bf985118980ce9eacea9d10"
97 | version = "v1.3.0"
98 |
99 | [[projects]]
100 | name = "gopkg.in/go-playground/validator.v8"
101 | packages = ["."]
102 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
103 | version = "v8.18.2"
104 |
105 | [[projects]]
106 | name = "gopkg.in/yaml.v2"
107 | packages = ["."]
108 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
109 | version = "v2.2.1"
110 |
111 | [solve-meta]
112 | analyzer-name = "dep"
113 | analyzer-version = 1
114 | inputs-digest = "3711685058a6f382d69f79c97a89dcd2a05d57872eb178ac4683ab12d9e52165"
115 | solver-name = "gps-cdcl"
116 | solver-version = 1
117 |
--------------------------------------------------------------------------------
/server/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 | #
22 | # [prune]
23 | # non-go = false
24 | # go-tests = true
25 | # unused-packages = true
26 |
27 |
28 | [prune]
29 | go-tests = true
30 | unused-packages = true
--------------------------------------------------------------------------------
/server/Makefile:
--------------------------------------------------------------------------------
1 | # パラメータ
2 | GOCMD=go
3 | GOBUILD=$(GOCMD) build
4 | GOCLEAN=$(GOCMD) clean
5 | GOTEST=$(GOCMD) test
6 | GOGET=$(GOCMD) get
7 | BINARY_NAME=gobinary
8 |
9 | .PHONY: init
10 | init: clean deps test precommit build
11 |
12 | .PHONY: all
13 | all: test precommit build run
14 |
15 | .PHONY: build
16 | build:
17 | $(GOBUILD) -o $(BINARY_NAME) -v
18 |
19 | .PHONY: test
20 | test:
21 | $(GOCMD) test ./...
22 |
23 | .PHONY: clean
24 | clean:
25 | $(GOCLEAN)
26 | rm -f $(BINARY_NAME)
27 |
28 | .PHONY: run
29 | run:
30 | ./$(BINARY_NAME)
31 |
32 | .PHONY: deps
33 | deps:
34 | $(GOGET) github.com/golang/dep/cmd/dep
35 | $(GOGET) github.com/gin-gonic/gin
36 | $(GOGET) golang.org/x/lint/golint
37 | $(GOGET) golang.org/x/tools/cmd/goimports
38 | $(GOGET) github.com/kisielk/errcheck
39 | $(GOGET) gopkg.in/DATA-DOG/go-sqlmock.v1
40 | $(GOGET) github.com/go-sql-driver/mysql
41 | dep ensure
42 |
43 | .PHONY: precommit
44 | precommit :
45 | # 静的解析
46 | go vet ./...
47 | # go list で import path を表示する
48 | # pipe を使用した後、 xargs でコマンドラインを作成し、xargs で作成したコマンドラインは、 go list の表示結果
49 | go list ./... | xargs golint -set_exit_status
50 | # エラーハンドリングの確認
51 | # test と Close の部分を無視している
52 | errcheck -ignoretests -ignore 'Close' ./...
53 |
--------------------------------------------------------------------------------
/server/adapter/api/api_helper.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
7 | "github.com/gin-gonic/gin"
8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/util"
9 | )
10 |
11 | // getID は、URLからIDの値を取得する。
12 | func getID(c *gin.Context) (int, error) {
13 | id, err := strconv.Atoi(c.Param(ID))
14 | if err != nil {
15 | return -1, &model.InvalidParameterError{
16 | Parameter: ID,
17 | Message: IDShouldBeIntErr,
18 | }
19 | }
20 |
21 | return id, nil
22 | }
23 |
24 | // getLimit は、Query StringからLimitの値を取得する。
25 | func getLimit(c *gin.Context) (int, error) {
26 | var err error
27 | limit := 20
28 |
29 | limitStr := c.Query(Limit)
30 | if !util.IsEmpty(limitStr){
31 | limit, err = strconv.Atoi(limitStr)
32 | if err != nil {
33 | return -1, &model.InvalidParameterError{
34 | Parameter: Limit,
35 | Message: LimitShouldBeIntErr,
36 | }
37 | }
38 | }
39 |
40 | return limit, nil
41 | }
42 |
43 | // ManageLimit は、Limitを制御する。
44 | func ManageLimit(targetLimit, maxLimit, minLimit, defaultLimit int) int {
45 | if maxLimit < targetLimit || targetLimit < minLimit {
46 | return defaultLimit
47 | }
48 | return targetLimit
49 | }
50 |
--------------------------------------------------------------------------------
/server/adapter/api/api_helper_test.go:
--------------------------------------------------------------------------------
1 | package api_test
2 |
3 | import (
4 | "testing"
5 | "github.com/SekiguchiKai/clean-architecture-with-go/server/adapter/api"
6 | )
7 |
8 | func TestManageLimit(t *testing.T) {
9 | type args struct {
10 | targetLimit int
11 | maxLimit int
12 | minLimit int
13 | defaultLimit int
14 | }
15 | tests := []struct {
16 | name string
17 | args args
18 | want int
19 | }{
20 | {
21 | name: "ターゲットが10、Maxが30、Minが5、Defaultが20のとき、10を返す",
22 | args: args{
23 | targetLimit: 10,
24 | maxLimit: 30,
25 | minLimit:5,
26 | defaultLimit: 20,
27 | },
28 | want: 10,
29 | },
30 | {
31 | name: "ターゲットが40、Maxが30、Defaultが20のとき、20を返す",
32 | args: args{
33 | targetLimit: 20,
34 | maxLimit: 30,
35 | defaultLimit: 20,
36 | },
37 | want: 20,
38 | },
39 | {
40 | name: "ターゲットが31、Maxが30、Defaultが20のとき、20を返す",
41 | args: args{
42 | targetLimit: 31,
43 | maxLimit: 30,
44 | defaultLimit: 20,
45 | },
46 | want: 20,
47 | },
48 | {
49 | name: "ターゲットが29、Maxが30、Defaultが20のとき、29を返す",
50 | args: args{
51 | targetLimit: 29,
52 | maxLimit: 30,
53 | defaultLimit: 29,
54 | },
55 | want: 29,
56 | },
57 | {
58 | name: "ターゲットが30、Maxが30、Defaultが20のとき、30を返す",
59 | args: args{
60 | targetLimit: 30,
61 | maxLimit: 30,
62 | defaultLimit: 30,
63 | },
64 | want: 30,
65 | },
66 | {
67 | name: "ターゲットが4、Maxが30、Minが5、Defaultが20のとき、20を返す",
68 | args: args{
69 | targetLimit: 4,
70 | maxLimit: 30,
71 | minLimit:5,
72 | defaultLimit: 20,
73 | },
74 | want: 20,
75 | },
76 | {
77 | name: "ターゲットが6、Maxが30、Minが5、Defaultが20のとき、6を返す",
78 | args: args{
79 | targetLimit: 6,
80 | maxLimit: 30,
81 | minLimit:5,
82 | defaultLimit: 20,
83 | },
84 | want: 6,
85 | },
86 | {
87 | name: "ターゲットが5、Maxが30、Minが5、Defaultが20のとき、5を返す",
88 | args: args{
89 | targetLimit: 5,
90 | maxLimit: 30,
91 | minLimit:5,
92 | defaultLimit: 20,
93 | },
94 | want: 5,
95 | },
96 | }
97 | for _, tt := range tests {
98 | t.Run(tt.name, func(t *testing.T) {
99 | if got := api.ManageLimit(tt.args.targetLimit, tt.args.maxLimit, tt.args.minLimit, tt.args.defaultLimit); got != tt.want {
100 | t.Errorf("ManageLimit() = %v, want %v", got, tt.want)
101 | }
102 | })
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/server/adapter/api/const.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | // パスの定義。
4 | const (
5 | ProgrammingLangAPIPath = "/langs"
6 | )
7 |
8 | // クエリストリングの属性。
9 | const (
10 | Limit = "limit"
11 | )
12 |
13 | // Limitの定義。
14 | const (
15 | MaxLimit = 100
16 | MinLimit = 5
17 | DefaultLimit = 20
18 | )
19 |
20 | // パラメータの属性
21 | const (
22 | ID = "id"
23 | )
24 |
25 | // HTTPのメソッド。
26 | const (
27 | Get = "GET"
28 | Post = "POST"
29 | Put = "PUT"
30 | Delete = "DELETE"
31 | )
32 |
--------------------------------------------------------------------------------
/server/adapter/api/err.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
7 | "github.com/pkg/errors"
8 | )
9 |
10 | // エラーの定数。
11 | const (
12 | OtherErr = "some error has occurred"
13 | IDShouldBeIntErr = "ID Should be int"
14 | LimitShouldBeIntErr = "Limit Should be int"
15 | )
16 |
17 | // handledError はハンドリング後のエラー。
18 | type handledError struct {
19 | code int
20 | message string
21 | }
22 |
23 | // handleError は、エラーをハンドリングする。
24 | func handleError(err error) *handledError {
25 | switch errors.Cause(err).(type) {
26 | case *model.NoSuchDataError:
27 | return &handledError{
28 | code: http.StatusNotFound,
29 | message: errors.Cause(err).Error(),
30 | }
31 | case *model.RequiredError:
32 | return &handledError{
33 | code: http.StatusBadRequest,
34 | message: errors.Cause(err).Error(),
35 | }
36 | case *model.InvalidPropertyError:
37 | return &handledError{
38 | code: http.StatusBadRequest,
39 | message: errors.Cause(err).Error(),
40 | }
41 | case *model.InvalidParameterError:
42 | return &handledError{
43 | code: http.StatusBadRequest,
44 | message: errors.Cause(err).Error(),
45 | }
46 | case *model.AlreadyExistError:
47 | return &handledError{
48 | code: http.StatusConflict,
49 | message: errors.Cause(err).Error(),
50 | }
51 | case *model.DBError:
52 | return &handledError{
53 | code: http.StatusInternalServerError,
54 | message: errors.Cause(err).Error(),
55 | }
56 | default:
57 | return &handledError{
58 | code: http.StatusInternalServerError,
59 | message: OtherErr,
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/server/adapter/api/program_lang_api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/input"
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | // ProgrammingLangAPI は、ProgrammingLangのAPI。
13 | type ProgrammingLangAPI struct {
14 | UseCase input.ProgrammingLangInputPort
15 | }
16 |
17 | // NewProgrammingLangAPI は、ProgrammingLangAPIを生成し、返す。
18 | func NewProgrammingLangAPI(useCase input.ProgrammingLangInputPort) *ProgrammingLangAPI {
19 | return &ProgrammingLangAPI{
20 | UseCase: useCase,
21 | }
22 | }
23 |
24 | // InitAPI は、APIを初期設定する。
25 | func (api *ProgrammingLangAPI) InitAPI(g *gin.RouterGroup) {
26 | g.GET(ProgrammingLangAPIPath, api.List)
27 | g.GET(fmt.Sprintf("%s/:%s", ProgrammingLangAPIPath, ID), api.Get)
28 | g.POST(ProgrammingLangAPIPath, api.Create)
29 | g.PUT(fmt.Sprintf("%s/:%s", ProgrammingLangAPIPath, ID), api.Update)
30 | g.DELETE(fmt.Sprintf("%s/:%s", ProgrammingLangAPIPath, ID), api.Delete)
31 | }
32 |
33 | // List は、ProgrammingLangの一覧を返す。
34 | func (api *ProgrammingLangAPI) List(c *gin.Context) {
35 | limit, err := getLimit(c)
36 | if err != nil {
37 | he := handleError(err)
38 | c.JSON(he.code, he.message)
39 | return
40 | }
41 |
42 | limit = ManageLimit(limit, MaxLimit, MinLimit, DefaultLimit)
43 |
44 | ctx := c.Request.Context()
45 | langSlice, err := api.UseCase.List(ctx, limit)
46 | if err != nil {
47 | he := handleError(err)
48 | c.JSON(he.code, he.message)
49 | return
50 | }
51 |
52 | c.JSON(http.StatusOK, langSlice)
53 | }
54 |
55 | // Get は、ProgrammingLangを取得する。
56 | func (api *ProgrammingLangAPI) Get(c *gin.Context) {
57 | id, err := getID(c)
58 | if err != nil {
59 | he := handleError(err)
60 | c.JSON(he.code, he.message)
61 | return
62 | }
63 |
64 | ctx := c.Request.Context()
65 | lang, err := api.UseCase.Get(ctx, id)
66 | if err != nil {
67 | he := handleError(err)
68 | c.JSON(he.code, he.message)
69 | return
70 | }
71 |
72 | c.JSON(http.StatusOK, lang)
73 | }
74 |
75 | // Create は、ProgrammingLangを生成する。
76 | func (api *ProgrammingLangAPI) Create(c *gin.Context) {
77 | var params *model.ProgrammingLang
78 | if err := c.BindJSON(¶ms); err != nil {
79 | c.JSON(http.StatusBadRequest, err.Error())
80 | return
81 | }
82 |
83 | ctx := c.Request.Context()
84 | lang, err := api.UseCase.Create(ctx, params)
85 | if err != nil {
86 | he := handleError(err)
87 | c.JSON(he.code, he.message)
88 | return
89 | }
90 |
91 | c.JSON(http.StatusOK, lang)
92 | }
93 |
94 | // Update は、ProgrammingLangを更新する。
95 | func (api *ProgrammingLangAPI) Update(c *gin.Context) {
96 | var params *model.ProgrammingLang
97 | if err := c.BindJSON(¶ms); err != nil {
98 | c.JSON(http.StatusBadRequest, err.Error())
99 | return
100 | }
101 |
102 | ctx := c.Request.Context()
103 | id, err := getID(c)
104 | if err != nil {
105 | he := handleError(err)
106 | c.JSON(he.code, he.message)
107 | return
108 | }
109 |
110 | lang, err := api.UseCase.Update(ctx, id, params)
111 | if err != nil {
112 | he := handleError(err)
113 | c.JSON(he.code, he.message)
114 | return
115 | }
116 |
117 | c.JSON(http.StatusOK, lang)
118 | }
119 |
120 | // Delete は、ProgrammingLangを削除する。
121 | func (api *ProgrammingLangAPI) Delete(c *gin.Context) {
122 | id, err := getID(c)
123 | if err != nil {
124 | he := handleError(err)
125 | c.JSON(he.code, he.message)
126 | return
127 | }
128 |
129 | ctx := c.Request.Context()
130 | if err := api.UseCase.Delete(ctx, id); err != nil {
131 | he := handleError(err)
132 | c.JSON(he.code, he.message)
133 | return
134 | }
135 |
136 | c.JSON(http.StatusOK, nil)
137 | }
138 |
--------------------------------------------------------------------------------
/server/adapter/api/program_lang_api_test.go:
--------------------------------------------------------------------------------
1 | package api_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "net/http/httptest"
9 | "reflect"
10 | "testing"
11 | "context"
12 |
13 | "github.com/SekiguchiKai/clean-architecture-with-go/server/adapter/api"
14 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
15 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/input"
16 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/mock"
17 | "github.com/SekiguchiKai/clean-architecture-with-go/server/util"
18 | "github.com/gin-gonic/gin"
19 | "github.com/golang/mock/gomock"
20 | )
21 |
22 | func TestNewProgrammingLangAPI(t *testing.T) {
23 | ctrl := gomock.NewController(t)
24 | defer ctrl.Finish()
25 |
26 | mock := mock_input.NewMockProgrammingLangInputPort(ctrl)
27 |
28 | type args struct {
29 | useCase input.ProgrammingLangInputPort
30 | }
31 | tests := []struct {
32 | name string
33 | args args
34 | }{
35 | {
36 | name: "適切な引数を与えるとProgrammingLangAPIを返すこと",
37 | args: args{
38 | useCase: mock,
39 | },
40 | },
41 | }
42 | for _, tt := range tests {
43 | t.Run(tt.name, func(t *testing.T) {
44 | if got := api.NewProgrammingLangAPI(tt.args.useCase); got == nil {
45 | t.Errorf("NewProgrammingLangAPI() = %v, want nil", got)
46 | }
47 | })
48 | }
49 | }
50 |
51 | func TestProgrammingLangAPI_List(t *testing.T) {
52 | ctrl := gomock.NewController(t)
53 | defer ctrl.Finish()
54 |
55 | u := mock_input.NewMockProgrammingLangInputPort(ctrl)
56 |
57 | langAPI := &api.ProgrammingLangAPI{
58 | UseCase: u,
59 | }
60 | handler := langAPI.List
61 |
62 | paramErr := &model.InvalidParameterError{
63 | Parameter: api.Limit,
64 | Message: api.LimitShouldBeIntErr,
65 | }
66 |
67 | dbErr := &model.DBError{
68 | ModelName: model.ModelNameProgrammingLang,
69 | DBMethod: model.DBMethodList,
70 | Detail: "Test",
71 | }
72 |
73 | type params struct {
74 | limit string
75 | }
76 |
77 | type mock struct {
78 | ctx context.Context
79 | limit int
80 | result []*model.ProgrammingLang
81 | err error
82 | }
83 |
84 | type want struct {
85 | code int
86 | result []*model.ProgrammingLang
87 | errMessage string
88 | }
89 |
90 | tests := []struct {
91 | name string
92 | params params
93 | mock mock
94 | want want
95 | }{
96 | {
97 | name: "リクエストのクエリパラメータが20で、データが20件以上存在する場合、ステータスコード200と20件のデータを返すこと",
98 | params:params{
99 | limit:"20",
100 | },
101 | mock: mock{
102 | ctx: context.Background(),
103 | limit: 20,
104 | result: model.CreateProgrammingLangs(20),
105 | err: nil,
106 | },
107 | want: want{
108 | code: http.StatusOK,
109 | result: model.CreateProgrammingLangs(20),
110 | },
111 | },
112 | {
113 | name: "リクエストのクエリパラメータが6で、データが6件以上存在する場合、ステータスコード200と6件のデータを返すこと",
114 | params:params{
115 | limit:"6",
116 | },
117 | mock: mock{
118 | ctx: context.Background(),
119 | limit: 6,
120 | result: model.CreateProgrammingLangs(6),
121 | err: nil,
122 | },
123 | want: want{
124 | code: http.StatusOK,
125 | result: model.CreateProgrammingLangs(6),
126 | },
127 | },
128 | {
129 | name: "リクエストのクエリパラメータが4で、データが20件以上存在する場合、ステータスコード200と20件のデータを返すこと",
130 | params:params{
131 | limit:"4",
132 | },
133 | mock: mock{
134 | ctx: context.Background(),
135 | limit: 20,
136 | result: model.CreateProgrammingLangs(20),
137 | err: nil,
138 | },
139 | want: want{
140 | code: http.StatusOK,
141 | result: model.CreateProgrammingLangs(20),
142 | },
143 | },
144 | {
145 | name: "リクエストのクエリパラメータが99で、データが99件以上存在する場合、ステータスコード200と99件のデータを返すこと",
146 | params:params{
147 | limit:"99",
148 | },
149 | mock: mock{
150 | ctx: context.Background(),
151 | limit: 99,
152 | result: model.CreateProgrammingLangs(99),
153 | err: nil,
154 | },
155 | want: want{
156 | code: http.StatusOK,
157 | result: model.CreateProgrammingLangs(99),
158 | },
159 | },
160 | {
161 | name: "リクエストのクエリパラメータが101で、データが20件以上存在する場合、ステータスコード200と20件のデータを返すこと",
162 | params:params{
163 | limit:"101",
164 | },
165 | mock: mock{
166 | ctx: context.Background(),
167 | limit: 20,
168 | result: model.CreateProgrammingLangs(20),
169 | err: nil,
170 | },
171 | want: want{
172 | code: http.StatusOK,
173 | result: model.CreateProgrammingLangs(20),
174 | },
175 | },
176 | {
177 | name: "リクエストのクエリパラメータの指定がなく、データが20件以上存在する場合、ステータスコード200と20件のデータを返すこと",
178 | params:params{
179 | limit:"",
180 | },
181 | mock: mock{
182 | ctx: context.Background(),
183 | limit: 20,
184 | result: model.CreateProgrammingLangs(20),
185 | err: nil,
186 | },
187 | want: want{
188 | code: http.StatusOK,
189 | result: model.CreateProgrammingLangs(20),
190 | },
191 | },
192 | {
193 | name: "リクエストのクエリパラメータが文字列の場合、ステータスコード400とエラーメッセージを返すこと",
194 | params:params{
195 | limit:"test",
196 | },
197 | mock: mock{
198 | ctx: context.Background(),
199 | result: nil,
200 | err: paramErr,
201 | },
202 | want: want{
203 | code: http.StatusBadRequest,
204 | result: nil,
205 | errMessage: paramErr.Error(),
206 | },
207 | },
208 | {
209 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと",
210 | params:params{
211 | limit:"20",
212 | },
213 | mock: mock{
214 | ctx: context.Background(),
215 | limit: 20,
216 | result: nil,
217 | err: dbErr,
218 | },
219 | want: want{
220 | code: http.StatusInternalServerError,
221 | result: nil,
222 | errMessage: dbErr.Error(),
223 | },
224 | },
225 | }
226 | for _, tt := range tests {
227 | t.Run(tt.name, func(t *testing.T) {
228 | r := gin.New()
229 | r.GET(api.ProgrammingLangAPIPath, handler)
230 |
231 | url := fmt.Sprintf("%s?%s=%s", api.ProgrammingLangAPIPath, api.Limit, tt.params.limit)
232 | if !reflect.DeepEqual(tt.mock.err, paramErr) {
233 | u.EXPECT().List(tt.mock.ctx, tt.mock.limit).Return(tt.mock.result, tt.mock.err)
234 | }
235 |
236 | rec := httptest.NewRecorder()
237 | req, err := http.NewRequest(api.Get, url, nil)
238 | if err != nil {
239 | t.Fatal(err)
240 | }
241 | r.ServeHTTP(rec, req)
242 |
243 | if tt.want.code == http.StatusOK {
244 | var got []*model.ProgrammingLang
245 | err = json.Unmarshal(rec.Body.Bytes(), &got)
246 | if err != nil {
247 | t.Fatal(err)
248 | }
249 |
250 | for i, v := range tt.want.result {
251 | if !reflect.DeepEqual(got[i], v) {
252 | t.Errorf("Response Body = %v, want %v", got[i], v)
253 | }
254 | }
255 |
256 | } else {
257 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage {
258 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage)
259 | }
260 | }
261 | if !reflect.DeepEqual(rec.Code, tt.want.code) {
262 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code)
263 | }
264 | })
265 | }
266 | }
267 |
268 | func TestProgrammingLangAPI_Get(t *testing.T) {
269 | ctrl := gomock.NewController(t)
270 | defer ctrl.Finish()
271 |
272 | u := mock_input.NewMockProgrammingLangInputPort(ctrl)
273 |
274 | langAPI := &api.ProgrammingLangAPI{
275 | UseCase: u,
276 | }
277 | handler := langAPI.Get
278 |
279 | noDataErr := &model.NoSuchDataError{
280 | ID: 1,
281 | Name: model.TestName,
282 | ModelName: model.ModelNameProgrammingLang,
283 | }
284 |
285 | dbErr := &model.DBError{
286 | ModelName: model.ModelNameProgrammingLang,
287 | DBMethod: model.DBMethodRead,
288 | Detail: "Test",
289 | }
290 |
291 | type mock struct {
292 | ctx context.Context
293 | id int
294 | result *model.ProgrammingLang
295 | err error
296 | }
297 |
298 | type want struct {
299 | code int
300 | result *model.ProgrammingLang
301 | errMessage string
302 | }
303 |
304 | tests := []struct {
305 | name string
306 | mock mock
307 | want want
308 | }{
309 | {
310 | name: "リクエストのURLのIDのパラメータが適切な場合、ステータスコード200と1件のデータを返すこと",
311 | mock: mock{
312 | ctx: context.Background(),
313 | id: 1,
314 | result: model.CreateProgrammingLangs(1)[0],
315 | err: nil,
316 | },
317 | want: want{
318 | code: http.StatusOK,
319 | result: model.CreateProgrammingLangs(1)[0],
320 | },
321 | },
322 | {
323 | name: "リクエストのURLのIDのパラメータと同一のIDを持つデータが存在しない場合、ステータスコード404とエラーメッセージを返すこと",
324 | mock: mock{
325 | ctx: context.Background(),
326 | id: 100,
327 | result: nil,
328 | err: noDataErr,
329 | },
330 | want: want{
331 | code: http.StatusNotFound,
332 | result: nil,
333 | errMessage: noDataErr.Error(),
334 | },
335 | },
336 | {
337 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと",
338 | mock: mock{
339 | ctx: context.Background(),
340 | id: 20,
341 | result: nil,
342 | err: dbErr,
343 | },
344 | want: want{
345 | code: http.StatusInternalServerError,
346 | result: nil,
347 | errMessage: dbErr.Error(),
348 | },
349 | },
350 | }
351 | for _, tt := range tests {
352 | t.Run(tt.name, func(t *testing.T) {
353 | r := gin.New()
354 | r.GET(fmt.Sprintf("%s/:%s", api.ProgrammingLangAPIPath, api.ID), handler)
355 | u.EXPECT().Get(tt.mock.ctx, tt.mock.id).Return(tt.mock.result, tt.mock.err)
356 |
357 | rec := httptest.NewRecorder()
358 | url := fmt.Sprintf("%s/%d", api.ProgrammingLangAPIPath, tt.mock.id)
359 | req, err := http.NewRequest(api.Get, url, nil)
360 | if err != nil {
361 | t.Fatal(err)
362 | }
363 | r.ServeHTTP(rec, req)
364 |
365 | if tt.want.code == http.StatusOK {
366 | var got *model.ProgrammingLang
367 | err = json.Unmarshal(rec.Body.Bytes(), &got)
368 | if err != nil {
369 | t.Fatal(err)
370 | }
371 | if !reflect.DeepEqual(got, tt.want.result) {
372 | t.Errorf("Response Body = %v, want %v", got, tt.want.result)
373 | }
374 |
375 | } else {
376 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage {
377 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage)
378 | }
379 | }
380 | if !reflect.DeepEqual(rec.Code, tt.want.code) {
381 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code)
382 | }
383 | })
384 | }
385 | }
386 |
387 | func TestProgrammingLangAPI_Create(t *testing.T) {
388 | ctrl := gomock.NewController(t)
389 | defer ctrl.Finish()
390 |
391 | u := mock_input.NewMockProgrammingLangInputPort(ctrl)
392 |
393 | langAPI := &api.ProgrammingLangAPI{
394 | UseCase: u,
395 | }
396 | handler := langAPI.Create
397 |
398 | conflictErr := &model.AlreadyExistError{
399 | ID: 1,
400 | Name: model.TestName,
401 | ModelName: model.ModelNameProgrammingLang,
402 | }
403 |
404 | dbErr := &model.DBError{
405 | ModelName: model.ModelNameProgrammingLang,
406 | DBMethod: model.DBMethodRead,
407 | Detail: "Test",
408 | }
409 |
410 | type mock struct {
411 | ctx context.Context
412 | param *model.ProgrammingLang
413 | result *model.ProgrammingLang
414 | err error
415 | }
416 |
417 | type want struct {
418 | code int
419 | result *model.ProgrammingLang
420 | errMessage string
421 | }
422 |
423 | tests := []struct {
424 | name string
425 | mock mock
426 | want want
427 | }{
428 | {
429 | name: "リクエストボディのProgrammingLangが適切な場合、ステータスコード200と1件のデータを返すこと",
430 | mock: mock{
431 | ctx: context.Background(),
432 | param: model.CreateProgrammingLangs(1)[0],
433 | result: model.CreateProgrammingLangs(1)[0],
434 | err: nil,
435 | },
436 | want: want{
437 | code: http.StatusOK,
438 | result: model.CreateProgrammingLangs(1)[0],
439 | },
440 | },
441 | {
442 | name: "リクエストボディのProgrammingLangが既に存在している場合、ステータスコード409とエラーメッセージを返すこと",
443 | mock: mock{
444 | ctx: context.Background(),
445 | param: model.CreateProgrammingLangs(1)[0],
446 | result: nil,
447 | err: conflictErr,
448 | },
449 | want: want{
450 | code: http.StatusConflict,
451 | result: nil,
452 | errMessage: conflictErr.Error(),
453 | },
454 | },
455 | {
456 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと",
457 | mock: mock{
458 | ctx: context.Background(),
459 | param: model.CreateProgrammingLangs(1)[0],
460 | result: nil,
461 | err: dbErr,
462 | },
463 | want: want{
464 | code: http.StatusInternalServerError,
465 | result: nil,
466 | errMessage: dbErr.Error(),
467 | },
468 | },
469 | }
470 | for _, tt := range tests {
471 | t.Run(tt.name, func(t *testing.T) {
472 | r := gin.New()
473 | r.POST(api.ProgrammingLangAPIPath, handler)
474 |
475 | u.EXPECT().Create(tt.mock.ctx, tt.mock.param).Return(tt.mock.result, tt.mock.err)
476 |
477 | rec := httptest.NewRecorder()
478 | b, err := json.Marshal(tt.mock.param)
479 | if err != nil {
480 | t.Fatal(err)
481 | }
482 | body := bytes.NewReader(b)
483 |
484 | req, err := http.NewRequest(api.Post, api.ProgrammingLangAPIPath, body)
485 | if err != nil {
486 | t.Fatal(err)
487 | }
488 | r.ServeHTTP(rec, req)
489 |
490 | if tt.want.code == http.StatusOK {
491 | var got *model.ProgrammingLang
492 | err = json.Unmarshal(rec.Body.Bytes(), &got)
493 | if err != nil {
494 | t.Fatal(err)
495 | }
496 | if !reflect.DeepEqual(got, tt.want.result) {
497 | t.Errorf("Response Body = %v, want %v", got, tt.want.result)
498 | }
499 |
500 | } else {
501 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage {
502 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage)
503 | }
504 | }
505 | if !reflect.DeepEqual(rec.Code, tt.want.code) {
506 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code)
507 | }
508 | })
509 | }
510 | }
511 |
512 | func TestProgrammingLangAPI_Update(t *testing.T) {
513 | ctrl := gomock.NewController(t)
514 | defer ctrl.Finish()
515 |
516 | u := mock_input.NewMockProgrammingLangInputPort(ctrl)
517 |
518 | langAPI := &api.ProgrammingLangAPI{
519 | UseCase: u,
520 | }
521 | handler := langAPI.Update
522 |
523 | noDataErr := &model.NoSuchDataError{
524 | ID: 1,
525 | Name: model.TestName,
526 | ModelName: model.ModelNameProgrammingLang,
527 | }
528 |
529 | dbErr := &model.DBError{
530 | ModelName: model.ModelNameProgrammingLang,
531 | DBMethod: model.DBMethodRead,
532 | Detail: "Test",
533 | }
534 |
535 | type mock struct {
536 | ctx context.Context
537 | id int
538 | param *model.ProgrammingLang
539 | result *model.ProgrammingLang
540 | err error
541 | }
542 |
543 | type want struct {
544 | code int
545 | result *model.ProgrammingLang
546 | errMessage string
547 | }
548 |
549 | type fields struct {
550 | UseCase input.ProgrammingLangInputPort
551 | }
552 |
553 | type param struct {
554 | id int
555 | }
556 |
557 | tests := []struct {
558 | name string
559 | fields fields
560 | mock mock
561 | want want
562 | param param
563 | }{
564 | {
565 | name: "リクエストボディのProgrammingLangが適切な場合、ステータスコード200と1件のデータを返すこと",
566 | mock: mock{
567 | ctx: context.Background(),
568 | id: 1,
569 | param: model.CreateProgrammingLangs(1)[0],
570 | result: model.CreateProgrammingLangs(1)[0],
571 | err: nil,
572 | },
573 | want: want{
574 | code: http.StatusOK,
575 | result: model.CreateProgrammingLangs(1)[0],
576 | },
577 | param: param{
578 | id: 1,
579 | },
580 | },
581 | {
582 | name: "リクエストのURLのIDのパラメータと同一のIDを持つデータが存在しない場合、ステータスコード404とエラーメッセージを返すこと",
583 | mock: mock{
584 | ctx: context.Background(),
585 | id: 100,
586 | param: model.CreateProgrammingLangs(1)[0],
587 | result: nil,
588 | err: noDataErr,
589 | },
590 | want: want{
591 | code: http.StatusNotFound,
592 | result: nil,
593 | errMessage: noDataErr.Error(),
594 | },
595 | param: param{
596 | id: 100,
597 | },
598 | },
599 | {
600 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと",
601 | mock: mock{
602 | ctx: context.Background(),
603 | id: 1,
604 | param: model.CreateProgrammingLangs(1)[0],
605 | result: nil,
606 | err: dbErr,
607 | },
608 | want: want{
609 | code: http.StatusInternalServerError,
610 | result: nil,
611 | errMessage: dbErr.Error(),
612 | },
613 | param: param{
614 | id: 1,
615 | },
616 | },
617 | }
618 | for _, tt := range tests {
619 | t.Run(tt.name, func(t *testing.T) {
620 | r := gin.New()
621 | r.PUT(fmt.Sprintf("%s/:%s", api.ProgrammingLangAPIPath, api.ID), handler)
622 |
623 | u.EXPECT().Update(tt.mock.ctx, tt.mock.id, tt.mock.param).Return(tt.mock.result, tt.mock.err)
624 |
625 | rec := httptest.NewRecorder()
626 | b, err := json.Marshal(tt.mock.param)
627 | if err != nil {
628 | t.Fatal(err)
629 | }
630 | body := bytes.NewReader(b)
631 |
632 | url := fmt.Sprintf("%s/%d", api.ProgrammingLangAPIPath, tt.param.id)
633 | req, err := http.NewRequest(api.Put, url, body)
634 | if err != nil {
635 | t.Fatal(err)
636 | }
637 | r.ServeHTTP(rec, req)
638 |
639 | if tt.want.code == http.StatusOK {
640 | var got *model.ProgrammingLang
641 | err = json.Unmarshal(rec.Body.Bytes(), &got)
642 | if err != nil {
643 | t.Fatal(err)
644 | }
645 | if !reflect.DeepEqual(got, tt.want.result) {
646 | t.Errorf("Response Body = %v, want %v", got, tt.want.result)
647 | }
648 |
649 | } else {
650 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage {
651 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage)
652 | }
653 | }
654 | if !reflect.DeepEqual(rec.Code, tt.want.code) {
655 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code)
656 | }
657 | })
658 | }
659 | }
660 |
661 | func TestProgrammingLangAPI_Delete(t *testing.T) {
662 | ctrl := gomock.NewController(t)
663 | defer ctrl.Finish()
664 |
665 | u := mock_input.NewMockProgrammingLangInputPort(ctrl)
666 |
667 | langAPI := &api.ProgrammingLangAPI{
668 | UseCase: u,
669 | }
670 | handler := langAPI.Delete
671 |
672 | noDataErr := &model.NoSuchDataError{
673 | ID: 1,
674 | Name: model.TestName,
675 | ModelName: model.ModelNameProgrammingLang,
676 | }
677 |
678 | dbErr := &model.DBError{
679 | ModelName: model.ModelNameProgrammingLang,
680 | DBMethod: model.DBMethodRead,
681 | Detail: "Test",
682 | }
683 |
684 | type mock struct {
685 | ctx context.Context
686 | id int
687 | err error
688 | }
689 |
690 | type want struct {
691 | code int
692 | result *model.ProgrammingLang
693 | errMessage string
694 | }
695 |
696 | type param struct {
697 | id int
698 | }
699 |
700 | tests := []struct {
701 | name string
702 | mock mock
703 | want want
704 | param param
705 | }{
706 | {
707 | name: "リクエストボディのProgrammingLangが適切な場合、ステータスコード200を返すこと",
708 | mock: mock{
709 | ctx: context.Background(),
710 | id: 1,
711 | err: nil,
712 | },
713 | want: want{
714 | code: http.StatusOK,
715 | result: nil,
716 | },
717 | param: param{
718 | id: 1,
719 | },
720 | },
721 | {
722 | name: "リクエストのURLのIDのパラメータと同一のIDを持つデータが存在しない場合、ステータスコード404とエラーメッセージを返すこと",
723 | mock: mock{
724 | ctx: context.Background(),
725 | id: 100,
726 | err: noDataErr,
727 | },
728 | want: want{
729 | code: http.StatusNotFound,
730 | result: nil,
731 | errMessage: noDataErr.Error(),
732 | },
733 | param: param{
734 | id: 100,
735 | },
736 | },
737 | {
738 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと",
739 | mock: mock{
740 | ctx: context.Background(),
741 | id: 1,
742 | err: dbErr,
743 | },
744 | want: want{
745 | code: http.StatusInternalServerError,
746 | result: nil,
747 | errMessage: dbErr.Error(),
748 | },
749 | param: param{
750 | id: 1,
751 | },
752 | },
753 | }
754 | for _, tt := range tests {
755 | t.Run(tt.name, func(t *testing.T) {
756 | r := gin.New()
757 | r.DELETE(fmt.Sprintf("%s/:%s", api.ProgrammingLangAPIPath, api.ID), handler)
758 |
759 | u.EXPECT().Delete(tt.mock.ctx, tt.mock.id).Return(tt.mock.err)
760 |
761 | url := fmt.Sprintf("%s/%d", api.ProgrammingLangAPIPath, tt.param.id)
762 |
763 | req, err := http.NewRequest(api.Delete, url, nil)
764 | if err != nil {
765 | t.Fatal(err)
766 | }
767 |
768 | rec := httptest.NewRecorder()
769 | r.ServeHTTP(rec, req)
770 |
771 | if tt.want.code == http.StatusOK {
772 | var got *model.ProgrammingLang
773 | err = json.Unmarshal(rec.Body.Bytes(), &got)
774 | if err != nil {
775 | t.Fatal(err)
776 | }
777 | if !reflect.DeepEqual(got, tt.want.result) {
778 | t.Errorf("Response Body = %v, want %v", got, tt.want.result)
779 | }
780 |
781 | } else {
782 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage {
783 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage)
784 | }
785 | }
786 | if !reflect.DeepEqual(rec.Code, tt.want.code) {
787 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code)
788 | }
789 | })
790 | }
791 | }
792 |
793 |
--------------------------------------------------------------------------------
/server/domain/model/const.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // プロパティの名称。
4 | const (
5 | PropertyName = "Name"
6 | )
7 |
8 | // エラー系。
9 | const (
10 | NameShouldBeMoreThanOneUnderTheTwenty = "Length of Name should be 0 < name < 21"
11 | )
12 |
13 | // エラー用の名称。
14 | const (
15 | ErrorProperty = "Property"
16 | ErrorMessage = "Message"
17 | )
18 |
19 | // モデル名。
20 | const (
21 | ModelNameProgrammingLang = "ProgrammingLang"
22 | )
23 |
24 | // テスト用の定数。
25 | const (
26 | TestName = "testName"
27 | TestFeature = "testFeature, testFeature, testFeature, testFeature, testFeature, testFeature, testFeature"
28 | TestDBSomeErr = "DB some error"
29 | )
30 |
31 | // DBの操作。
32 | const (
33 | DBMethodCreate = "Create"
34 | DBMethodList = "List"
35 | DBMethodRead = "Read"
36 | DBMethodUpdate = "Update"
37 | DBMethodDelete = "Delete"
38 | )
39 |
--------------------------------------------------------------------------------
/server/domain/model/error.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "fmt"
4 |
5 | // RequiredError は、必要なものが存在しない場合のエラー。
6 | type RequiredError struct {
7 | Property string
8 | }
9 |
10 | // Error は、エラー文を返す。
11 | func (e *RequiredError) Error() string {
12 | return fmt.Sprintf("%s is required", e.Property)
13 | }
14 |
15 | // InvalidPropertyError は、Propertyが不適切な場合のエラー。
16 | type InvalidPropertyError struct {
17 | Property string
18 | Message string
19 | }
20 |
21 | // Error は、エラー文を返す。
22 | func (e *InvalidPropertyError) Error() string {
23 | return fmt.Sprintf("%s is invalid. %s", e.Property, e.Message)
24 | }
25 |
26 | // InvalidParameterError は、Parameterが不適切な場合のエラー。
27 | type InvalidParameterError struct {
28 | Parameter string
29 | Message string
30 | }
31 |
32 | // Error は、エラー文を返す。
33 | func (e *InvalidParameterError) Error() string {
34 | return fmt.Sprintf("%s is invalid. %s", e.Parameter, e.Message)
35 | }
36 |
37 | // DBError は、DBのエラーを表す。
38 | type DBError struct {
39 | ModelName string
40 | DBMethod string
41 | Detail string
42 | }
43 |
44 | // Error は、エラー文を返す。
45 | func (e *DBError) Error() string {
46 | return fmt.Sprintf("failed DB operation. Method : %s, Model : %s, Detail : %s", e.DBMethod, e.ModelName, e.Detail)
47 | }
48 |
49 | // AlreadyExistError は、既に存在することを表すエラー。
50 | type AlreadyExistError struct {
51 | ID int
52 | Name string
53 | ModelName string
54 | }
55 |
56 | // Error は、エラーメッセージを返却する。
57 | func (e *AlreadyExistError) Error() string {
58 | return fmt.Sprintf("already exists. model: %s, id: %d, name: %s", e.ModelName, e.ID, e.Name)
59 | }
60 |
61 | // NoSuchDataError は、データが存在しないことを表すエラー。
62 | type NoSuchDataError struct {
63 | ID int
64 | Name string
65 | ModelName string
66 | }
67 |
68 | // Error は、エラーメッセージを返す。
69 | func (e *NoSuchDataError) Error() string {
70 | return fmt.Sprintf("no such model.model: %s, id: %d, name: %s", e.ModelName, e.ID, e.Name)
71 | }
72 |
--------------------------------------------------------------------------------
/server/domain/model/programming_lang.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | // ProgrammingLang は、プログラミング言語を表す。
6 | type ProgrammingLang struct {
7 | ID int `json:"id"`
8 | Name string `json:"name"`
9 | Feature string `json:"feature"`
10 | CreatedAt time.Time `json:"createdAt"`
11 | UpdatedAt time.Time `json:"updatedAt"`
12 | }
13 |
--------------------------------------------------------------------------------
/server/domain/model/test_helper.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | // GetTestTime は、テスト用の時刻を生成し、返す。
9 | func GetTestTime(month time.Month, day int) time.Time {
10 | return time.Date(2018, month, day, 12, 0, 0, 0, time.UTC)
11 | }
12 |
13 | // CreateProgrammingLangs は、引数で与えられた数だけProgrammingLangを生成し、返す。
14 | func CreateProgrammingLangs(num int) []*ProgrammingLang {
15 | langSlice := make([]*ProgrammingLang, num, num)
16 | for i := range langSlice {
17 | langSlice[i] = &ProgrammingLang{
18 | ID: i + 1,
19 | Name: fmt.Sprintf("%s%d", TestName, i),
20 | Feature: fmt.Sprintf("%s%d", TestFeature, i),
21 | CreatedAt: GetTestTime(time.October, i+1),
22 | UpdatedAt: GetTestTime(time.October, i+1),
23 | }
24 | }
25 | return langSlice
26 | }
27 |
--------------------------------------------------------------------------------
/server/domain/repository/programming_lang_repository.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
7 | )
8 |
9 | // ProgrammingLangRepository は、ProgrammingLangのRepository。
10 | type ProgrammingLangRepository interface {
11 | List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error)
12 | Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error)
13 | Read(ctx context.Context, id int) (*model.ProgrammingLang, error)
14 | ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error)
15 | Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error)
16 | Delete(ctx context.Context, id int) error
17 | }
18 |
--------------------------------------------------------------------------------
/server/domain/service/program_lang_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
5 | "github.com/pkg/errors"
6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/util"
7 | )
8 |
9 | // NewProgrammingLang は、ProgrammingLangを生成し、返す。
10 | func NewProgrammingLang(name string) (*model.ProgrammingLang, error) {
11 | if err := ValidateProgrammingLang(name); err != nil {
12 | return nil, errors.WithStack(err)
13 | }
14 |
15 | return &model.ProgrammingLang{
16 | Name: name,
17 | }, nil
18 | }
19 |
20 | // ValidateProgrammingLang は、ProgrammingLangの生成に必要な属性に与えられる引数をチェックする。
21 | func ValidateProgrammingLang(name string) error {
22 | if util.IsEmpty(name) || len(name) > 20 {
23 | return &model.InvalidPropertyError{
24 | Property: model.PropertyName,
25 | Message: model.NameShouldBeMoreThanOneUnderTheTwenty,
26 | }
27 | }
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/server/domain/service/program_lang_service_test.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
7 | )
8 |
9 | func TestNewProgrammingLang(t *testing.T) {
10 | type args struct {
11 | name string
12 | }
13 |
14 | type wantErr struct {
15 | isErr bool
16 | err error
17 | }
18 |
19 | invalidPropertyErr := &model.InvalidPropertyError{
20 | Property: model.PropertyName,
21 | Message: model.NameShouldBeMoreThanOneUnderTheTwenty,
22 | }
23 |
24 | tests := []struct {
25 | name string
26 | args args
27 | want *model.ProgrammingLang
28 | wantErr wantErr
29 | }{
30 | {
31 | name: "Nameが20文字の場合、ProgrammingLangを返す",
32 | args: args{
33 | name: "abcdefghijklmnopqrst",
34 | },
35 | wantErr: wantErr{
36 | isErr: false,
37 | },
38 | },
39 | {
40 | name: "Nameが21文字の場合、エラーを返す",
41 | args: args{
42 | name: "abcdefghijklmnopqrstu",
43 | },
44 | wantErr: wantErr{
45 | isErr: true,
46 | err: invalidPropertyErr,
47 | },
48 | },
49 | {
50 | name: "Nameが空文字の場合、エラーを返す",
51 | args: args{
52 | name: "",
53 | },
54 | wantErr: wantErr{
55 | isErr: true,
56 | err: invalidPropertyErr,
57 | },
58 | },
59 | }
60 | for _, tt := range tests {
61 | t.Run(tt.name, func(t *testing.T) {
62 | got, err := NewProgrammingLang(tt.args.name)
63 | if (err != nil) != tt.wantErr.isErr {
64 | t.Errorf("NewProgrammingLang() error = %v, wantErr %v", err, tt.wantErr.isErr)
65 | return
66 | }
67 |
68 | if tt.wantErr.isErr {
69 | if err.Error() != tt.wantErr.err.Error() {
70 | t.Errorf("ValidateProgrammingLang() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error())
71 | }
72 | } else {
73 | if got == nil {
74 | t.Errorf("NewProgrammingLang() = %v, want not nil", got)
75 | }
76 | }
77 | })
78 | }
79 | }
80 |
81 | func TestValidateProgrammingLang(t *testing.T) {
82 | type args struct {
83 | name string
84 | }
85 |
86 | type wantErr struct {
87 | isErr bool
88 | err error
89 | }
90 |
91 | invalidPropertyErr := &model.InvalidPropertyError{
92 | Property: model.PropertyName,
93 | Message: model.NameShouldBeMoreThanOneUnderTheTwenty,
94 | }
95 |
96 | tests := []struct {
97 | name string
98 | args args
99 | wantErr wantErr
100 | }{
101 | {
102 | name: "Nameが20文字の場合、エラーを返さない",
103 | args: args{
104 | name: "abcdefghijklmnopqrst",
105 | },
106 | wantErr: wantErr{
107 | isErr: false,
108 | },
109 | },
110 | {
111 | name: "Nameが21文字の場合、エラーを返す",
112 | args: args{
113 | name: "abcdefghijklmnopqrstu",
114 | },
115 | wantErr: wantErr{
116 | isErr: true,
117 | err: invalidPropertyErr,
118 | },
119 | },
120 | {
121 | name: "Nameが空文字の場合、エラーを返す",
122 | args: args{
123 | name: "",
124 | },
125 | wantErr: wantErr{
126 | isErr: true,
127 | err: invalidPropertyErr,
128 | },
129 | },
130 | }
131 | for _, tt := range tests {
132 | t.Run(tt.name, func(t *testing.T) {
133 | err := ValidateProgrammingLang(tt.args.name)
134 | if (err != nil) != tt.wantErr.isErr {
135 | t.Errorf("ValidateProgrammingLang() error = %v, wantErr %v", err, tt.wantErr.isErr)
136 | }
137 |
138 | if tt.wantErr.isErr {
139 | if err.Error() != tt.wantErr.err.Error() {
140 | t.Errorf("ValidateProgrammingLang() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error())
141 | }
142 | }
143 | })
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/server/infra/dao/mock/programming_lang_repository_mock.go:
--------------------------------------------------------------------------------
1 | // Code generated by MockGen. DO NOT EDIT.
2 | // Source: domain/repository/programming_lang_repository.go
3 |
4 | // Package mock_repository is a generated GoMock package.
5 | package mock_repository
6 |
7 | import (
8 | context "context"
9 | model "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
10 | gomock "github.com/golang/mock/gomock"
11 | reflect "reflect"
12 | )
13 |
14 | // MockProgrammingLangRepository is a mock of ProgrammingLangRepository interface
15 | type MockProgrammingLangRepository struct {
16 | ctrl *gomock.Controller
17 | recorder *MockProgrammingLangRepositoryMockRecorder
18 | }
19 |
20 | // MockProgrammingLangRepositoryMockRecorder is the mock recorder for MockProgrammingLangRepository
21 | type MockProgrammingLangRepositoryMockRecorder struct {
22 | mock *MockProgrammingLangRepository
23 | }
24 |
25 | // NewMockProgrammingLangRepository creates a new mock instance
26 | func NewMockProgrammingLangRepository(ctrl *gomock.Controller) *MockProgrammingLangRepository {
27 | mock := &MockProgrammingLangRepository{ctrl: ctrl}
28 | mock.recorder = &MockProgrammingLangRepositoryMockRecorder{mock}
29 | return mock
30 | }
31 |
32 | // EXPECT returns an object that allows the caller to indicate expected use
33 | func (m *MockProgrammingLangRepository) EXPECT() *MockProgrammingLangRepositoryMockRecorder {
34 | return m.recorder
35 | }
36 |
37 | // List mocks base method
38 | func (m *MockProgrammingLangRepository) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) {
39 | ret := m.ctrl.Call(m, "List", ctx, limit)
40 | ret0, _ := ret[0].([]*model.ProgrammingLang)
41 | ret1, _ := ret[1].(error)
42 | return ret0, ret1
43 | }
44 |
45 | // List indicates an expected call of List
46 | func (mr *MockProgrammingLangRepositoryMockRecorder) List(ctx, limit interface{}) *gomock.Call {
47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProgrammingLangRepository)(nil).List), ctx, limit)
48 | }
49 |
50 | // Create mocks base method
51 | func (m *MockProgrammingLangRepository) Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) {
52 | ret := m.ctrl.Call(m, "Create", ctx, lang)
53 | ret0, _ := ret[0].(*model.ProgrammingLang)
54 | ret1, _ := ret[1].(error)
55 | return ret0, ret1
56 | }
57 |
58 | // Create indicates an expected call of Create
59 | func (mr *MockProgrammingLangRepositoryMockRecorder) Create(ctx, lang interface{}) *gomock.Call {
60 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Create), ctx, lang)
61 | }
62 |
63 | // Read mocks base method
64 | func (m *MockProgrammingLangRepository) Read(ctx context.Context, id int) (*model.ProgrammingLang, error) {
65 | ret := m.ctrl.Call(m, "Read", ctx, id)
66 | ret0, _ := ret[0].(*model.ProgrammingLang)
67 | ret1, _ := ret[1].(error)
68 | return ret0, ret1
69 | }
70 |
71 | // Read indicates an expected call of Read
72 | func (mr *MockProgrammingLangRepositoryMockRecorder) Read(ctx, id interface{}) *gomock.Call {
73 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Read), ctx, id)
74 | }
75 |
76 | // ReadByName mocks base method
77 | func (m *MockProgrammingLangRepository) ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error) {
78 | ret := m.ctrl.Call(m, "ReadByName", ctx, name)
79 | ret0, _ := ret[0].(*model.ProgrammingLang)
80 | ret1, _ := ret[1].(error)
81 | return ret0, ret1
82 | }
83 |
84 | // ReadByName indicates an expected call of ReadByName
85 | func (mr *MockProgrammingLangRepositoryMockRecorder) ReadByName(ctx, name interface{}) *gomock.Call {
86 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadByName", reflect.TypeOf((*MockProgrammingLangRepository)(nil).ReadByName), ctx, name)
87 | }
88 |
89 | // Update mocks base method
90 | func (m *MockProgrammingLangRepository) Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) {
91 | ret := m.ctrl.Call(m, "Update", ctx, lang)
92 | ret0, _ := ret[0].(*model.ProgrammingLang)
93 | ret1, _ := ret[1].(error)
94 | return ret0, ret1
95 | }
96 |
97 | // Update indicates an expected call of Update
98 | func (mr *MockProgrammingLangRepositoryMockRecorder) Update(ctx, lang interface{}) *gomock.Call {
99 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Update), ctx, lang)
100 | }
101 |
102 | // Delete mocks base method
103 | func (m *MockProgrammingLangRepository) Delete(ctx context.Context, id int) error {
104 | ret := m.ctrl.Call(m, "Delete", ctx, id)
105 | ret0, _ := ret[0].(error)
106 | return ret0
107 | }
108 |
109 | // Delete indicates an expected call of Delete
110 | func (mr *MockProgrammingLangRepositoryMockRecorder) Delete(ctx, id interface{}) *gomock.Call {
111 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Delete), ctx, id)
112 | }
113 |
--------------------------------------------------------------------------------
/server/infra/dao/rdb/const.go:
--------------------------------------------------------------------------------
1 | package rdb
2 |
3 | // rdb packageの定数。
4 | const (
5 | TotalAffected = "Total Affected"
6 | )
7 |
--------------------------------------------------------------------------------
/server/infra/dao/rdb/program_lang_dao.go:
--------------------------------------------------------------------------------
1 | package rdb
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository"
9 | "github.com/pkg/errors"
10 | )
11 |
12 | // ProgrammingLangDAO は、ProgrammingLangのDAO。
13 | type ProgrammingLangDAO struct {
14 | SQLManager SQLManagerInterface
15 | }
16 |
17 | // NewProgrammingLangDAO は、ProgrammingLangDAO生成して返す。
18 | func NewProgrammingLangDAO(manager SQLManagerInterface) repository.ProgrammingLangRepository {
19 | fmt.Printf("NewProgrammingLangDAO")
20 |
21 | return &ProgrammingLangDAO{
22 | SQLManager: manager,
23 | }
24 | }
25 |
26 | // ErrorMsg は、エラー文を生成し、返す。
27 | func (dao *ProgrammingLangDAO) ErrorMsg(method string, err error) error {
28 | return &model.DBError{
29 | ModelName: model.ModelNameProgrammingLang,
30 | DBMethod: method,
31 | Detail: err.Error(),
32 | }
33 | }
34 |
35 | // Create は、レコードを1件生成する。
36 | func (dao *ProgrammingLangDAO) Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) {
37 | query := "INSERT INTO programming_langs (name, feature, created_at, updated_at) VALUES (?, ?, ?, ?)"
38 | stmt, err := dao.SQLManager.PrepareContext(ctx, query)
39 | if err != nil {
40 | return nil, dao.ErrorMsg(model.DBMethodCreate, err)
41 | }
42 | defer stmt.Close()
43 |
44 | result, err := stmt.ExecContext(ctx, lang.Name, lang.Feature, lang.CreatedAt, lang.UpdatedAt)
45 | if err != nil {
46 | return nil, dao.ErrorMsg(model.DBMethodCreate, err)
47 | }
48 |
49 | affect, err := result.RowsAffected()
50 | if affect != 1 {
51 | err = fmt.Errorf("%s: %d ", TotalAffected, affect)
52 | return nil, dao.ErrorMsg(model.DBMethodUpdate, err)
53 | }
54 |
55 | id, err := result.LastInsertId()
56 | if err != nil {
57 | return nil, dao.ErrorMsg(model.DBMethodCreate, err)
58 | }
59 |
60 | lang.ID = int(id)
61 |
62 | return lang, nil
63 | }
64 |
65 | // List は、レコードの一覧を取得して返す。
66 | func (dao *ProgrammingLangDAO) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) {
67 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs ORDER BY name LIMIT ?"
68 | langSlice, err := dao.list(ctx, query, limit)
69 |
70 | if len(langSlice) == 0 {
71 | return nil, &model.NoSuchDataError{
72 | ModelName: model.ModelNameProgrammingLang,
73 | }
74 | }
75 |
76 | if err != nil {
77 | return nil, errors.WithStack(err)
78 | }
79 |
80 | return langSlice, nil
81 | }
82 |
83 | // Read は、レコードを1件取得して返す。
84 | func (dao *ProgrammingLangDAO) Read(ctx context.Context, id int) (*model.ProgrammingLang, error) {
85 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE ID=?"
86 |
87 | langSlice, err := dao.list(ctx, query, id)
88 |
89 | if len(langSlice) == 0 {
90 | return nil, &model.NoSuchDataError{
91 | ID: id,
92 | ModelName: model.ModelNameProgrammingLang,
93 | }
94 | }
95 |
96 | if err != nil {
97 | return nil, errors.WithStack(err)
98 | }
99 |
100 | return langSlice[0], nil
101 | }
102 |
103 | // ReadByName は、指定したNameを保持するレコードを1返す。
104 | func (dao *ProgrammingLangDAO) ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error) {
105 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE name=? ORDER BY name LIMIT ?"
106 | langSlice, err := dao.list(ctx, query, name, 1)
107 |
108 | if len(langSlice) == 0 {
109 | return nil, &model.NoSuchDataError{
110 | Name: name,
111 | ModelName: model.ModelNameProgrammingLang,
112 | }
113 | }
114 |
115 | if err != nil {
116 | return nil, errors.WithStack(err)
117 | }
118 |
119 | return langSlice[0], nil
120 | }
121 |
122 | // list は、レコードの一覧を取得して返す。
123 | func (dao *ProgrammingLangDAO) list(ctx context.Context, query string, args ...interface{}) ([]*model.ProgrammingLang, error) {
124 | stmt, err := dao.SQLManager.PrepareContext(ctx, query)
125 | if err != nil {
126 | return nil, dao.ErrorMsg(model.DBMethodList, err)
127 | }
128 | defer stmt.Close()
129 |
130 | rows, err := stmt.QueryContext(ctx, args...)
131 | if err != nil {
132 | return nil, dao.ErrorMsg(model.DBMethodList, err)
133 | }
134 | defer rows.Close()
135 |
136 | langSlice := make([]*model.ProgrammingLang, 0)
137 | for rows.Next() {
138 | lang := &model.ProgrammingLang{}
139 |
140 | err = rows.Scan(
141 | &lang.ID,
142 | &lang.Name,
143 | &lang.Feature,
144 | &lang.CreatedAt,
145 | &lang.UpdatedAt,
146 | )
147 |
148 | if err != nil {
149 | return nil, dao.ErrorMsg(model.DBMethodList, err)
150 | }
151 |
152 | langSlice = append(langSlice, lang)
153 | }
154 |
155 | return langSlice, nil
156 | }
157 |
158 | // Update は、レコードを1件更新する。
159 | func (dao *ProgrammingLangDAO) Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) {
160 | query := "UPDATE programming_langs SET name=?, feature=?, created_at=?, updated_at=? WHERE id=?"
161 |
162 | stmt, err := dao.SQLManager.PrepareContext(ctx, query)
163 | defer stmt.Close()
164 |
165 | if err != nil {
166 | return nil, dao.ErrorMsg(model.DBMethodUpdate, err)
167 | }
168 |
169 | result, err := stmt.ExecContext(ctx, lang.Name, lang.Feature, lang.CreatedAt, lang.UpdatedAt, lang.ID)
170 | if err != nil {
171 | return nil, dao.ErrorMsg(model.DBMethodUpdate, err)
172 | }
173 |
174 | affect, err := result.RowsAffected()
175 | if affect != 1 {
176 | err = fmt.Errorf("%s: %d ", TotalAffected, affect)
177 | return nil, dao.ErrorMsg(model.DBMethodUpdate, err)
178 | }
179 |
180 | return lang, nil
181 | }
182 |
183 | // Delete は、レコードを1件削除する。
184 | func (dao *ProgrammingLangDAO) Delete(ctx context.Context, id int) error {
185 | query := "DELETE FROM programming_langs WHERE id=?"
186 |
187 | stmt, err := dao.SQLManager.PrepareContext(ctx, query)
188 | if err != nil {
189 | return dao.ErrorMsg(model.DBMethodDelete, err)
190 | }
191 | defer stmt.Close()
192 |
193 | result, err := stmt.ExecContext(ctx, id)
194 | if err != nil {
195 | return dao.ErrorMsg(model.DBMethodDelete, err)
196 | }
197 |
198 | affect, err := result.RowsAffected()
199 | if err != nil {
200 | return dao.ErrorMsg(model.DBMethodDelete, err)
201 | }
202 | if affect != 1 {
203 | err = fmt.Errorf("%s: %d ", TotalAffected, affect)
204 | return dao.ErrorMsg(model.DBMethodDelete, err)
205 | }
206 |
207 | return nil
208 | }
209 |
--------------------------------------------------------------------------------
/server/infra/dao/rdb/program_lang_dao_test.go:
--------------------------------------------------------------------------------
1 | package rdb_test
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "fmt"
7 | "reflect"
8 | "testing"
9 | "time"
10 |
11 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
12 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository"
13 | "github.com/SekiguchiKai/clean-architecture-with-go/server/infra/dao/rdb"
14 | "gopkg.in/DATA-DOG/go-sqlmock.v1"
15 | )
16 |
17 | func TestNewProgrammingLangDAO(t *testing.T) {
18 | type args struct {
19 | ctx context.Context
20 | manager rdb.SQLManagerInterface
21 | }
22 | tests := []struct {
23 | name string
24 | args args
25 | want repository.ProgrammingLangRepository
26 | }{
27 | {
28 | name: "適切な引数を与えると、ProgrammingLangDAOが返されること",
29 | args: args{
30 | ctx: context.Background(),
31 | manager: &rdb.SQLManager{
32 | Conn: &sql.DB{},
33 | },
34 | },
35 | },
36 | }
37 | for _, tt := range tests {
38 | t.Run(tt.name, func(t *testing.T) {
39 | if got := rdb.NewProgrammingLangDAO(tt.args.manager); got == nil {
40 | t.Errorf("NewProgrammingLangDAO() = nil")
41 | }
42 | })
43 | }
44 | }
45 |
46 | func TestProgrammingLangDAO_Create(t *testing.T) {
47 | // sqlmockの設定を行う
48 | db, mock, err := sqlmock.New()
49 | if err != nil {
50 | t.Fatal(err)
51 | }
52 |
53 | defer db.Close()
54 |
55 | type fields struct {
56 | SQLManager rdb.SQLManagerInterface
57 | }
58 | type args struct {
59 | ctx context.Context
60 | lang *model.ProgrammingLang
61 | }
62 | tests := []struct {
63 | name string
64 | fields fields
65 | args args
66 | want *model.ProgrammingLang
67 | rowAffected int64
68 | wantErr bool
69 | }{
70 | {
71 | name: "NameとFeatureを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと",
72 | fields: fields{
73 | SQLManager: &rdb.SQLManager{Conn: db},
74 | },
75 | args: args{
76 | ctx: context.Background(),
77 | lang: &model.ProgrammingLang{
78 | Name: model.TestName,
79 | Feature: model.TestFeature,
80 | CreatedAt: model.GetTestTime(time.September, 1),
81 | UpdatedAt: model.GetTestTime(time.September, 2),
82 | },
83 | },
84 | want: &model.ProgrammingLang{
85 | ID: 1,
86 | Name: model.TestName,
87 | Feature: model.TestFeature,
88 | CreatedAt: model.GetTestTime(time.September, 1),
89 | UpdatedAt: model.GetTestTime(time.September, 2),
90 | },
91 | rowAffected: 1,
92 | wantErr: false,
93 | },
94 | {
95 | name: "Nameのみを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと",
96 | fields: fields{
97 | SQLManager: &rdb.SQLManager{Conn: db},
98 | },
99 | args: args{
100 | ctx: context.Background(),
101 | lang: &model.ProgrammingLang{
102 | Name: model.TestName,
103 | CreatedAt: model.GetTestTime(time.September, 1),
104 | UpdatedAt: model.GetTestTime(time.September, 2),
105 | },
106 | },
107 | want: &model.ProgrammingLang{
108 | ID: 1,
109 | Name: model.TestName,
110 | CreatedAt: model.GetTestTime(time.September, 1),
111 | UpdatedAt: model.GetTestTime(time.September, 2),
112 | },
113 | rowAffected: 1,
114 | wantErr: false,
115 | },
116 | {
117 | name: "RowAffectedが1以外の場合、エラーを返すこと",
118 | fields: fields{
119 | SQLManager: &rdb.SQLManager{Conn: db},
120 | },
121 | args: args{
122 | ctx: context.Background(),
123 | lang: &model.ProgrammingLang{
124 | Name: model.TestName,
125 | CreatedAt: model.GetTestTime(time.September, 1),
126 | UpdatedAt: model.GetTestTime(time.September, 2),
127 | },
128 | },
129 | want: nil,
130 | rowAffected: 2,
131 | wantErr: true,
132 | },
133 | {
134 | name: "空のProgrammingLangを与えられた場合、エラーを返すこと",
135 | fields: fields{
136 | SQLManager: &rdb.SQLManager{Conn: db},
137 | },
138 | args: args{
139 | ctx: context.Background(),
140 | lang: &model.ProgrammingLang{},
141 | },
142 | want: nil,
143 | rowAffected: 0,
144 | wantErr: true,
145 | },
146 | }
147 | for _, tt := range tests {
148 | t.Run(tt.name, func(t *testing.T) {
149 | query := "INSERT INTO programming_langs"
150 | prep := mock.ExpectPrepare(query)
151 |
152 | if tt.rowAffected == 0 {
153 | prep.ExpectExec().WithArgs(tt.args.lang.Name, tt.args.lang.Feature, tt.args.lang.CreatedAt, tt.args.lang.UpdatedAt).WillReturnError(fmt.Errorf(model.TestDBSomeErr))
154 | } else {
155 | prep.ExpectExec().WithArgs(tt.args.lang.Name, tt.args.lang.Feature, tt.args.lang.CreatedAt, tt.args.lang.UpdatedAt).WillReturnResult(sqlmock.NewResult(1, tt.rowAffected))
156 | }
157 |
158 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager)
159 |
160 | got, err := dao.Create(tt.args.ctx, tt.args.lang)
161 | if (err != nil) != tt.wantErr {
162 | t.Errorf("ProgrammingLangDAO.Create() error = %v, wantErr %v", err, tt.wantErr)
163 | return
164 | }
165 | if !reflect.DeepEqual(got, tt.want) {
166 | t.Errorf("ProgrammingLangDAO.Create() = %v, want %v", got, tt.want)
167 | }
168 | })
169 | }
170 | }
171 |
172 | func TestProgrammingLangDAO_List(t *testing.T) {
173 | testName1 := fmt.Sprintf("%s1", model.TestName)
174 | testName2 := fmt.Sprintf("%s2", model.TestName)
175 | testName3 := fmt.Sprintf("%s3", model.TestName)
176 |
177 | testFeature1 := fmt.Sprintf("%s1", model.TestFeature)
178 | testFeature2 := fmt.Sprintf("%s2", model.TestFeature)
179 | testFeature3 := fmt.Sprintf("%s3", model.TestFeature)
180 |
181 | // sqlmockの設定を行う
182 | db, mock, err := sqlmock.New()
183 | if err != nil {
184 | t.Fatal(err)
185 | }
186 |
187 | defer db.Close()
188 |
189 | type fields struct {
190 | SQLManager rdb.SQLManagerInterface
191 | }
192 | type args struct {
193 | ctx context.Context
194 | limit int
195 | }
196 |
197 | tests := []struct {
198 | name string
199 | fields fields
200 | args args
201 | want []*model.ProgrammingLang
202 | wantErr bool
203 | }{
204 | {
205 | name: "NameとFeatureを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと",
206 | fields: fields{
207 | SQLManager: &rdb.SQLManager{Conn: db},
208 | },
209 | args: args{
210 | ctx: context.Background(),
211 | limit: 100,
212 | },
213 | want: []*model.ProgrammingLang{
214 | {
215 | ID: 1,
216 | Name: testName1,
217 | Feature: testFeature1,
218 | CreatedAt: model.GetTestTime(time.September, 1),
219 | UpdatedAt: model.GetTestTime(time.September, 2),
220 | },
221 | {
222 | ID: 2,
223 | Name: testName2,
224 | Feature: testFeature2,
225 | CreatedAt: model.GetTestTime(time.September, 3),
226 | UpdatedAt: model.GetTestTime(time.September, 4),
227 | },
228 | {
229 | ID: 3,
230 | Name: testName3,
231 | Feature: testFeature3,
232 | CreatedAt: model.GetTestTime(time.September, 5),
233 | UpdatedAt: model.GetTestTime(time.September, 6),
234 | },
235 | },
236 | wantErr: false,
237 | },
238 | {
239 | name: "NameとFeatureを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと",
240 | fields: fields{
241 | SQLManager: &rdb.SQLManager{Conn: db},
242 | },
243 | args: args{
244 | ctx: context.Background(),
245 | limit: 100,
246 | },
247 | want: nil,
248 | wantErr: true,
249 | },
250 | }
251 | for _, tt := range tests {
252 | t.Run(tt.name, func(t *testing.T) {
253 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs ORDER BY name LIMIT \\?"
254 | prep := mock.ExpectPrepare(query)
255 |
256 | if tt.wantErr {
257 | prep.ExpectQuery().WillReturnError(fmt.Errorf(model.TestDBSomeErr))
258 | } else {
259 | rows := sqlmock.NewRows([]string{"id", "name", "feature", "created_at", "updated_at"}).
260 | AddRow(tt.want[0].ID, tt.want[0].Name, tt.want[0].Feature, tt.want[0].CreatedAt, tt.want[0].UpdatedAt).
261 | AddRow(tt.want[1].ID, tt.want[1].Name, tt.want[1].Feature, tt.want[1].CreatedAt, tt.want[1].UpdatedAt).
262 | AddRow(tt.want[2].ID, tt.want[2].Name, tt.want[2].Feature, tt.want[2].CreatedAt, tt.want[2].UpdatedAt)
263 | prep.ExpectQuery().WillReturnRows(rows)
264 | }
265 |
266 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager)
267 |
268 | got, err := dao.List(tt.args.ctx, tt.args.limit)
269 | if (err != nil) != tt.wantErr {
270 | t.Errorf("ProgrammingLangDAO.List() error = %v, wantErr %v", err, tt.wantErr)
271 | return
272 | }
273 |
274 | for i := range got {
275 | if !reflect.DeepEqual(got[i], tt.want[i]) {
276 | t.Errorf("ProgrammingLangDAO.List() = %v, want %v", got[i], tt.want[i])
277 | }
278 | }
279 | })
280 | }
281 | }
282 |
283 | func TestProgrammingLangDAO_Read(t *testing.T) {
284 | // sqlmockの設定を行う
285 | db, mock, err := sqlmock.New()
286 | if err != nil {
287 | t.Fatal(err)
288 | }
289 |
290 | defer db.Close()
291 |
292 | type fields struct {
293 | SQLManager rdb.SQLManagerInterface
294 | }
295 | type args struct {
296 | ctx context.Context
297 | id int
298 | }
299 | tests := []struct {
300 | name string
301 | fields fields
302 | args args
303 | want *model.ProgrammingLang
304 | wantErr bool
305 | }{
306 | {
307 | name: "IDで指定したProgrammingLangが存在する場合、ProgrammingLangを1件返すこと",
308 | fields: fields{
309 | SQLManager: &rdb.SQLManager{Conn: db},
310 | },
311 | args: args{
312 | ctx: context.Background(),
313 | id: 1,
314 | },
315 | want: &model.ProgrammingLang{
316 | ID: 1,
317 | Name: model.TestName,
318 | Feature: model.TestFeature,
319 | CreatedAt: model.GetTestTime(time.September, 1),
320 | UpdatedAt: model.GetTestTime(time.September, 2),
321 | },
322 | wantErr: false,
323 | },
324 | {
325 | name: "IDで指定したProgrammingLangが存在しない場合、エラー返すこと",
326 | fields: fields{
327 | SQLManager: &rdb.SQLManager{Conn: db},
328 | },
329 | args: args{
330 | ctx: context.Background(),
331 | id: 2,
332 | },
333 | want: nil,
334 | wantErr: true,
335 | },
336 | }
337 | for _, tt := range tests {
338 | t.Run(tt.name, func(t *testing.T) {
339 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE ID=\\?"
340 | prep := mock.ExpectPrepare(query)
341 |
342 | if tt.wantErr {
343 | prep.ExpectQuery().WillReturnError(fmt.Errorf(model.TestDBSomeErr))
344 | } else {
345 | rows := sqlmock.NewRows([]string{"id", "name", "feature", "created_at", "updated_at"}).
346 | AddRow(tt.want.ID, tt.want.Name, tt.want.Feature, tt.want.CreatedAt, tt.want.UpdatedAt)
347 | prep.ExpectQuery().WillReturnRows(rows)
348 | }
349 |
350 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager)
351 |
352 | got, err := dao.Read(tt.args.ctx, tt.args.id)
353 | if (err != nil) != tt.wantErr {
354 | t.Errorf("ProgrammingLangDAO.Read() error = %v, wantErr %v", err, tt.wantErr)
355 | return
356 | }
357 | if !reflect.DeepEqual(got, tt.want) {
358 | t.Errorf("ProgrammingLangDAO.Read() = %v, want %v", got, tt.want)
359 | }
360 | })
361 | }
362 | }
363 |
364 | func TestProgrammingLangDAO_ReadByName(t *testing.T) {
365 | // sqlmockの設定を行う
366 | db, mock, err := sqlmock.New()
367 | if err != nil {
368 | t.Fatal(err)
369 | }
370 |
371 | defer db.Close()
372 |
373 | type fields struct {
374 | SQLManager rdb.SQLManagerInterface
375 | }
376 | type args struct {
377 | ctx context.Context
378 | name string
379 | }
380 | tests := []struct {
381 | name string
382 | fields fields
383 | args args
384 | want *model.ProgrammingLang
385 | wantErr bool
386 | }{
387 | {
388 | name: "IDで指定したProgrammingLangが1件存在する場合、ProgrammingLangを1件返すこと",
389 | fields: fields{
390 | SQLManager: &rdb.SQLManager{Conn: db},
391 | },
392 | args: args{
393 | ctx: context.Background(),
394 | name: model.TestName,
395 | },
396 | want: &model.ProgrammingLang{
397 | ID: 1,
398 | Name: model.TestName,
399 | Feature: model.TestFeature,
400 | CreatedAt: model.GetTestTime(time.September, 1),
401 | UpdatedAt: model.GetTestTime(time.September, 2),
402 | },
403 | wantErr: false,
404 | },
405 | {
406 | name: "Nameで指定したProgrammingLangが存在しない場合、エラー返すこと",
407 | fields: fields{
408 | SQLManager: &rdb.SQLManager{Conn: db},
409 | },
410 | args: args{
411 | ctx: context.Background(),
412 | name: "test",
413 | },
414 | want: nil,
415 | wantErr: true,
416 | },
417 | }
418 | for _, tt := range tests {
419 | t.Run(tt.name, func(t *testing.T) {
420 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE name=\\? ORDER BY name LIMIT \\?"
421 | prep := mock.ExpectPrepare(query)
422 |
423 | if tt.wantErr {
424 | prep.ExpectQuery().WillReturnError(fmt.Errorf(model.TestDBSomeErr))
425 | } else {
426 | rows := sqlmock.NewRows([]string{"id", "name", "feature", "created_at", "updated_at"}).
427 | AddRow(tt.want.ID, tt.want.Name, tt.want.Feature, tt.want.CreatedAt, tt.want.UpdatedAt)
428 | prep.ExpectQuery().WillReturnRows(rows)
429 | }
430 |
431 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager)
432 |
433 | got, err := dao.ReadByName(tt.args.ctx, tt.args.name)
434 | if (err != nil) != tt.wantErr {
435 | t.Errorf("ProgrammingLangDAO.ReadByName() error = %v, wantErr %v", err, tt.wantErr)
436 | return
437 | }
438 | if err == nil {
439 | if !reflect.DeepEqual(got, tt.want) {
440 | t.Errorf("ProgrammingLangDAO.ReadByName() = %v, want %v", got, tt.want)
441 | }
442 | }
443 | })
444 | }
445 | }
446 |
447 | func TestProgrammingLangDAO_Update(t *testing.T) {
448 | // sqlmockの設定を行う
449 | db, mock, err := sqlmock.New()
450 | if err != nil {
451 | t.Fatal(err)
452 | }
453 |
454 | defer db.Close()
455 |
456 | type fields struct {
457 | SQLManager rdb.SQLManagerInterface
458 | }
459 | type args struct {
460 | ctx context.Context
461 | lang *model.ProgrammingLang
462 | }
463 | tests := []struct {
464 | name string
465 | fields fields
466 | args args
467 | want *model.ProgrammingLang
468 | rowAffected int64
469 | wantErr bool
470 | }{
471 | {
472 | name: "NameとFeatureを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと",
473 | fields: fields{
474 | SQLManager: &rdb.SQLManager{Conn: db},
475 | },
476 | args: args{
477 | ctx: context.Background(),
478 | lang: &model.ProgrammingLang{
479 | ID: 1,
480 | Name: model.TestName,
481 | Feature: model.TestFeature,
482 | CreatedAt: model.GetTestTime(time.September, 1),
483 | UpdatedAt: model.GetTestTime(time.September, 2),
484 | },
485 | },
486 | want: &model.ProgrammingLang{
487 | ID: 1,
488 | Name: model.TestName,
489 | Feature: model.TestFeature,
490 | CreatedAt: model.GetTestTime(time.September, 1),
491 | UpdatedAt: model.GetTestTime(time.September, 2),
492 | },
493 | rowAffected: 1,
494 | wantErr: false,
495 | },
496 | {
497 | name: "Nameのみを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと",
498 | fields: fields{
499 | SQLManager: &rdb.SQLManager{Conn: db},
500 | },
501 | args: args{
502 | ctx: context.Background(),
503 | lang: &model.ProgrammingLang{
504 | ID: 1,
505 | Name: model.TestName,
506 | CreatedAt: model.GetTestTime(time.September, 1),
507 | UpdatedAt: model.GetTestTime(time.September, 2),
508 | },
509 | },
510 | want: &model.ProgrammingLang{
511 | ID: 1,
512 | Name: model.TestName,
513 | CreatedAt: model.GetTestTime(time.September, 1),
514 | UpdatedAt: model.GetTestTime(time.September, 2),
515 | },
516 | rowAffected: 1,
517 | wantErr: false,
518 | },
519 | {
520 | name: "RowAffectedが1以外の場合、エラーを返すこと",
521 | fields: fields{
522 | SQLManager: &rdb.SQLManager{Conn: db},
523 | },
524 | args: args{
525 | ctx: context.Background(),
526 | lang: &model.ProgrammingLang{
527 | Name: model.TestName,
528 | CreatedAt: model.GetTestTime(time.September, 1),
529 | UpdatedAt: model.GetTestTime(time.September, 2),
530 | },
531 | },
532 | want: nil,
533 | rowAffected: 2,
534 | wantErr: true,
535 | },
536 | {
537 | name: "空のProgrammingLangを与えられた場合、エラーを返すこと",
538 | fields: fields{
539 | SQLManager: &rdb.SQLManager{Conn: db},
540 | },
541 | args: args{
542 | ctx: context.Background(),
543 | lang: &model.ProgrammingLang{},
544 | },
545 | want: nil,
546 | rowAffected: 0,
547 | wantErr: true,
548 | },
549 | }
550 | for _, tt := range tests {
551 | t.Run(tt.name, func(t *testing.T) {
552 | query := "UPDATE programming_langs SET name=\\?, feature=\\?, created_at=\\?, updated_at=\\? WHERE id=\\?"
553 | prep := mock.ExpectPrepare(query)
554 |
555 | if tt.wantErr {
556 | prep.ExpectExec().WithArgs(tt.args.lang.Name, tt.args.lang.Feature, tt.args.lang.CreatedAt, tt.args.lang.UpdatedAt, tt.args.lang.ID).WillReturnError(fmt.Errorf(model.TestDBSomeErr))
557 | } else {
558 | prep.ExpectExec().WithArgs(tt.args.lang.Name, tt.args.lang.Feature, tt.args.lang.CreatedAt, tt.args.lang.UpdatedAt, tt.args.lang.ID).WillReturnResult(sqlmock.NewResult(1, tt.rowAffected))
559 | }
560 |
561 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager)
562 |
563 | got, err := dao.Update(tt.args.ctx, tt.args.lang)
564 | if (err != nil) != tt.wantErr {
565 | t.Errorf("ProgrammingLangDAO.Update() error = %v, wantErr %v", err, tt.wantErr)
566 | return
567 | }
568 | if !reflect.DeepEqual(got, tt.want) {
569 | t.Errorf("ProgrammingLangDAO.Update() = %v, want %v", got, tt.want)
570 | }
571 | })
572 | }
573 | }
574 |
575 | func TestProgrammingLangDAO_Delete(t *testing.T) {
576 | // sqlmockの設定を行う
577 | db, mock, err := sqlmock.New()
578 | if err != nil {
579 | t.Fatal(err)
580 | }
581 |
582 | defer db.Close()
583 |
584 | type fields struct {
585 | SQLManager rdb.SQLManagerInterface
586 | }
587 | type args struct {
588 | ctx context.Context
589 | id int
590 | }
591 | tests := []struct {
592 | name string
593 | fields fields
594 | args args
595 | rowAffected int64
596 | wantErr bool
597 | }{
598 | {
599 | name: "適切なIDを与えると、エラーを返さないこと",
600 | fields: fields{
601 | SQLManager: &rdb.SQLManager{Conn: db},
602 | },
603 | args: args{
604 | ctx: context.Background(),
605 | id: 1,
606 | },
607 | rowAffected: 1,
608 | wantErr: false,
609 | },
610 | {
611 | name: "rowAffectedが1以外の場合、エラーを返すこと",
612 | fields: fields{
613 | SQLManager: &rdb.SQLManager{Conn: db},
614 | },
615 | args: args{
616 | ctx: context.Background(),
617 | id: 1,
618 | },
619 | rowAffected: 2,
620 | wantErr: true,
621 | },
622 | }
623 | for _, tt := range tests {
624 | t.Run(tt.name, func(t *testing.T) {
625 | query := "DELETE FROM programming_langs WHERE id=\\?"
626 | prep := mock.ExpectPrepare(query)
627 |
628 | if tt.wantErr {
629 | prep.ExpectExec().WithArgs(tt.args.id).WillReturnError(fmt.Errorf(model.TestDBSomeErr))
630 | } else {
631 | prep.ExpectExec().WithArgs(tt.args.id).WillReturnResult(sqlmock.NewResult(1, tt.rowAffected))
632 | }
633 |
634 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager)
635 |
636 | if err := dao.Delete(tt.args.ctx, tt.args.id); (err != nil) != tt.wantErr {
637 | t.Errorf("ProgrammingLangDAO.Delete() error = %v, wantErr %v", err, tt.wantErr)
638 | return
639 | }
640 | })
641 | }
642 | }
643 |
--------------------------------------------------------------------------------
/server/infra/dao/rdb/sql_manager.go:
--------------------------------------------------------------------------------
1 | package rdb
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | // SQL Driver。
7 | _ "github.com/go-sql-driver/mysql"
8 | )
9 |
10 | // SQLManager は、SQLを管理する。
11 | type SQLManager struct {
12 | Conn *sql.DB
13 | }
14 |
15 | // NewSQLManager は、SQLManagerを生成し、返す。
16 | func NewSQLManager() SQLManagerInterface {
17 | conn, err := sql.Open("mysql", "root:@tcp(db:3306)/sample?charset=utf8mb4&parseTime=True")
18 | if err != nil {
19 | panic(err.Error())
20 | }
21 |
22 | return &SQLManager{
23 | Conn: conn,
24 | }
25 | }
26 |
27 | // Exec は、SQL実行する。
28 | func (s *SQLManager) Exec(query string, args ...interface{}) (Result, error) {
29 | return s.Conn.Exec(query, args...)
30 | }
31 |
32 | // ExecContext は、SQL実行する。
33 | func (s *SQLManager) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) {
34 | return s.Conn.ExecContext(ctx, query, args...)
35 | }
36 |
37 | // Query は、rowを返すようなQueryを実行する。
38 | func (s *SQLManager) Query(query string, args ...interface{}) (Rows, error) {
39 | rows, err := s.Conn.Query(query, args...)
40 | if err != nil {
41 | return nil, err
42 | }
43 | row := &SQLRowManager{
44 | Rows: rows,
45 | }
46 | return row, nil
47 | }
48 |
49 | // QueryContext は、rowを返すようなQueryを実行する。
50 | func (s *SQLManager) QueryContext(ctx context.Context, query string, args ...interface{}) (Rows, error) {
51 | rows, err := s.Conn.Query(query, args...)
52 | if err != nil {
53 | return nil, err
54 | }
55 | row := &SQLRowManager{
56 | Rows: rows,
57 | }
58 | return row, nil
59 | }
60 |
61 | // Prepare は、後でQueryやExecを行うために、準備された状態にする。
62 | func (s *SQLManager) Prepare(query string) (*sql.Stmt, error) {
63 | return s.Conn.Prepare(query)
64 | }
65 |
66 | // PrepareContext は、後でQueryやExecを行うために、準備された状態にする。
67 | func (s *SQLManager) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
68 | return s.Conn.PrepareContext(ctx, query)
69 | }
70 |
71 | // SQLRowManager は、Rowを管理する。
72 | type SQLRowManager struct {
73 | Rows *sql.Rows
74 | }
75 |
76 | // Scan は、destに現在読み込んでいるrowのcolumnsをコピーする。
77 | func (r SQLRowManager) Scan(dest ...interface{}) error {
78 | return r.Rows.Scan(dest...)
79 | }
80 |
81 | // Next は、Scanによって読み込まれる次のrowの結果を準備する。
82 | func (r SQLRowManager) Next() bool {
83 | return r.Rows.Next()
84 | }
85 |
86 | // Close は、Rowsを終了する。
87 | func (r SQLRowManager) Close() error {
88 | return r.Rows.Close()
89 | }
90 |
--------------------------------------------------------------------------------
/server/infra/dao/rdb/sql_manager_interface.go:
--------------------------------------------------------------------------------
1 | package rdb
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | )
7 |
8 | // SQLManagerInterface は、SQLManagerのinterface。
9 | type SQLManagerInterface interface {
10 | Executor
11 | Preparer
12 | Queryer
13 | }
14 |
15 | // DBに関するInterfaceの定義。
16 | type (
17 | // Executor は、Rowが返ってこないQueryを実行するためのメソッドを集めたinterface。
18 | Executor interface {
19 | // Exec は、Rowが返ってこないQueryを実行する
20 | Exec(query string, args ...interface{}) (Result, error)
21 | // ExecContext は、Rowが返ってこないQueryを実行する
22 | ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error)
23 | }
24 |
25 | // Preparer は、後でQueryやExecを行うために、準備された状態にするメソッドを集めたinterface。
26 | Preparer interface {
27 | // Prepare は、後でQueryやExecを行うために、準備された状態にする。
28 | // callerは、準備された状態が必要ない場合には、Closeを呼び出す必要がある。
29 | Prepare(query string) (*sql.Stmt, error)
30 | // PrepareContext は、後でQueryやExecを行うために、準備された状態にする。
31 | // callerは、準備された状態が必要ない場合には、Closeを呼び出す必要がある。
32 | // 引数で渡されたcontextは、状態の準備に使用するのであって、状態の実行に使用するのではない。
33 | PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
34 | }
35 |
36 | // Queryer は、rowを返すようなQueryを実行するメソッドを集めたinterface。
37 | Queryer interface {
38 | // Query は、rowを返すようなQueryを実行する。SELECTが典型的な例。
39 | Query(query string, args ...interface{}) (Rows, error)
40 | // QueryContext は、rowを返すようなQueryを実行する。SELECTが典型的な例。
41 | QueryContext(ctx context.Context, query string, args ...interface{}) (Rows, error)
42 | }
43 |
44 | // Row は、1行を選択するQueryRowの結果を表す。
45 | Row interface {
46 | // Scan は、destに現在読み込んでいるrowのcolumnsをコピーする。
47 | Scan(...interface{}) error
48 | }
49 |
50 | // Rows は、Queryの結果を表す。
51 | Rows interface {
52 | Row
53 | // Next は、Scanによって読み込まれる次のrowの結果を準備する。
54 | // Nextが成功した場合には、trueを返して、失敗した場合にはfalseを返す。
55 | Next() bool
56 | // Close は、Rowsを終了する。
57 | // Nextが呼ばれて、falseが返ってきて結果がそれ以上ない場合は、Rowsは自動でCloseする。
58 | // Errの結果を確認するにはそれで十分である。
59 | // Closeを使うと冪等になるし、Errの結果に影響を受けなくなる。
60 | Close() error
61 | }
62 |
63 | // Result は、DBからのレスポンスを扱うメソッドを集めたinterface。
64 | Result interface {
65 | // LastInsertId は、DBのレスポンスによって生成された数字を返す。
66 | LastInsertId() (int64, error)
67 | // RowsAffected は、update、insert、deleteで影響を受けたrowの数を返す。
68 | RowsAffected() (int64, error)
69 | }
70 | )
71 |
--------------------------------------------------------------------------------
/server/infra/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/SekiguchiKai/clean-architecture-with-go/server/adapter/api"
5 | "github.com/SekiguchiKai/clean-architecture-with-go/server/infra/dao/rdb"
6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase"
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | // G は、ginのフレームワークのインスタンス。
11 | var G *gin.Engine
12 |
13 | // init は、アプリケーションの初期設定を行う。
14 | func init() {
15 | g := gin.New()
16 | apiV1 := g.Group("/v1")
17 |
18 | sqlM := rdb.NewSQLManager()
19 | langAPI := initProgrammingLang(sqlM)
20 | langAPI.InitAPI(apiV1)
21 |
22 | G = g
23 | }
24 |
25 | // initProgrammingLang は、ProgrammingLangに関する初期設定を行う。
26 | func initProgrammingLang(sqlM rdb.SQLManagerInterface) *api.ProgrammingLangAPI {
27 | rep := rdb.NewProgrammingLangDAO(sqlM)
28 | u := usecase.NewProgrammingLangUseCase(rep)
29 | api := api.NewProgrammingLangAPI(u)
30 | return api
31 | }
32 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/SekiguchiKai/clean-architecture-with-go/server/infra/router"
5 | )
6 |
7 | func main() {
8 | if err := router.G.Run(":8080"); err != nil {
9 | panic(err.Error())
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/usecase/input/programming_lang_input.go:
--------------------------------------------------------------------------------
1 | package input
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
7 | )
8 |
9 | // ProgrammingLangInputPort は、ProgrammingLangのInputPort。
10 | type ProgrammingLangInputPort interface {
11 | List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error)
12 | Get(ctx context.Context, id int) (*model.ProgrammingLang, error)
13 | Create(ctx context.Context, param *model.ProgrammingLang) (*model.ProgrammingLang, error)
14 | Update(ctx context.Context, id int, param *model.ProgrammingLang) (*model.ProgrammingLang, error)
15 | Delete(ctx context.Context, id int) error
16 | }
17 |
--------------------------------------------------------------------------------
/server/usecase/mock/programming_lang_input_mock.go:
--------------------------------------------------------------------------------
1 | // Code generated by MockGen. DO NOT EDIT.
2 | // Source: usecase/input/programming_lang_input.go
3 |
4 | // Package mock_input is a generated GoMock package.
5 | package mock_input
6 |
7 | import (
8 | context "context"
9 | model "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
10 | gomock "github.com/golang/mock/gomock"
11 | reflect "reflect"
12 | )
13 |
14 | // MockProgrammingLangInputPort is a mock of ProgrammingLangInputPort interface
15 | type MockProgrammingLangInputPort struct {
16 | ctrl *gomock.Controller
17 | recorder *MockProgrammingLangInputPortMockRecorder
18 | }
19 |
20 | // MockProgrammingLangInputPortMockRecorder is the mock recorder for MockProgrammingLangInputPort
21 | type MockProgrammingLangInputPortMockRecorder struct {
22 | mock *MockProgrammingLangInputPort
23 | }
24 |
25 | // NewMockProgrammingLangInputPort creates a new mock instance
26 | func NewMockProgrammingLangInputPort(ctrl *gomock.Controller) *MockProgrammingLangInputPort {
27 | mock := &MockProgrammingLangInputPort{ctrl: ctrl}
28 | mock.recorder = &MockProgrammingLangInputPortMockRecorder{mock}
29 | return mock
30 | }
31 |
32 | // EXPECT returns an object that allows the caller to indicate expected use
33 | func (m *MockProgrammingLangInputPort) EXPECT() *MockProgrammingLangInputPortMockRecorder {
34 | return m.recorder
35 | }
36 |
37 | // List mocks base method
38 | func (m *MockProgrammingLangInputPort) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) {
39 | ret := m.ctrl.Call(m, "List", ctx, limit)
40 | ret0, _ := ret[0].([]*model.ProgrammingLang)
41 | ret1, _ := ret[1].(error)
42 | return ret0, ret1
43 | }
44 |
45 | // List indicates an expected call of List
46 | func (mr *MockProgrammingLangInputPortMockRecorder) List(ctx, limit interface{}) *gomock.Call {
47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).List), ctx, limit)
48 | }
49 |
50 | // Get mocks base method
51 | func (m *MockProgrammingLangInputPort) Get(ctx context.Context, id int) (*model.ProgrammingLang, error) {
52 | ret := m.ctrl.Call(m, "Get", ctx, id)
53 | ret0, _ := ret[0].(*model.ProgrammingLang)
54 | ret1, _ := ret[1].(error)
55 | return ret0, ret1
56 | }
57 |
58 | // Get indicates an expected call of Get
59 | func (mr *MockProgrammingLangInputPortMockRecorder) Get(ctx, id interface{}) *gomock.Call {
60 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).Get), ctx, id)
61 | }
62 |
63 | // Create mocks base method
64 | func (m *MockProgrammingLangInputPort) Create(ctx context.Context, param *model.ProgrammingLang) (*model.ProgrammingLang, error) {
65 | ret := m.ctrl.Call(m, "Create", ctx, param)
66 | ret0, _ := ret[0].(*model.ProgrammingLang)
67 | ret1, _ := ret[1].(error)
68 | return ret0, ret1
69 | }
70 |
71 | // Create indicates an expected call of Create
72 | func (mr *MockProgrammingLangInputPortMockRecorder) Create(ctx, param interface{}) *gomock.Call {
73 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).Create), ctx, param)
74 | }
75 |
76 | // Update mocks base method
77 | func (m *MockProgrammingLangInputPort) Update(ctx context.Context, id int, param *model.ProgrammingLang) (*model.ProgrammingLang, error) {
78 | ret := m.ctrl.Call(m, "Update", ctx, id, param)
79 | ret0, _ := ret[0].(*model.ProgrammingLang)
80 | ret1, _ := ret[1].(error)
81 | return ret0, ret1
82 | }
83 |
84 | // Update indicates an expected call of Update
85 | func (mr *MockProgrammingLangInputPortMockRecorder) Update(ctx, id, param interface{}) *gomock.Call {
86 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).Update), ctx, id, param)
87 | }
88 |
89 | // Delete mocks base method
90 | func (m *MockProgrammingLangInputPort) Delete(ctx context.Context, id int) error {
91 | ret := m.ctrl.Call(m, "Delete", ctx, id)
92 | ret0, _ := ret[0].(error)
93 | return ret0
94 | }
95 |
96 | // Delete indicates an expected call of Delete
97 | func (mr *MockProgrammingLangInputPortMockRecorder) Delete(ctx, id interface{}) *gomock.Call {
98 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).Delete), ctx, id)
99 | }
100 |
--------------------------------------------------------------------------------
/server/usecase/program_lang_usecase.go:
--------------------------------------------------------------------------------
1 | package usecase
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository"
9 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/input"
10 | "github.com/pkg/errors"
11 | )
12 |
13 | // ProgrammingLangUseCase は、ProgrammingLangのUseCase。
14 | type ProgrammingLangUseCase struct {
15 | Repo repository.ProgrammingLangRepository
16 | }
17 |
18 | // NewProgrammingLangUseCase は、ProgrammingLangUseCaseを生成し、返す。
19 | func NewProgrammingLangUseCase(repo repository.ProgrammingLangRepository) input.ProgrammingLangInputPort {
20 | return &ProgrammingLangUseCase{
21 | Repo: repo,
22 | }
23 | }
24 |
25 | // List は、ProgrammingLangの一覧を返す。
26 | func (u *ProgrammingLangUseCase) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) {
27 | return u.Repo.List(ctx, limit)
28 | }
29 |
30 | // Get は、ProgrammingLang1件返す。
31 | func (u *ProgrammingLangUseCase) Get(ctx context.Context, id int) (*model.ProgrammingLang, error) {
32 | return u.Repo.Read(ctx, id)
33 | }
34 |
35 | // Create は、ProgrammingLangを生成する。
36 | func (u *ProgrammingLangUseCase) Create(ctx context.Context, param *model.ProgrammingLang) (*model.ProgrammingLang, error) {
37 | lang, err := u.Repo.ReadByName(ctx, param.Name)
38 | if lang != nil {
39 | return nil, &model.AlreadyExistError{
40 | ID: lang.ID,
41 | Name: lang.Name,
42 | ModelName: model.ModelNameProgrammingLang,
43 | }
44 | }
45 |
46 | if _, ok := errors.Cause(err).(*model.NoSuchDataError); !ok {
47 | return nil, errors.WithStack(err)
48 | }
49 |
50 | param.CreatedAt = time.Now().UTC()
51 | param.UpdatedAt = time.Now().UTC()
52 |
53 | lang, err = u.Repo.Create(ctx, param)
54 | if err != nil {
55 | return nil, errors.WithStack(err)
56 | }
57 |
58 | return lang, nil
59 | }
60 |
61 | // Update は、ProgrammingLangを更新する。
62 | func (u *ProgrammingLangUseCase) Update(ctx context.Context, id int, param *model.ProgrammingLang) (*model.ProgrammingLang, error) {
63 | lang, err := u.Repo.Read(ctx, id)
64 | if lang == nil {
65 | return nil, &model.NoSuchDataError{
66 | ID: id,
67 | Name: param.Name,
68 | ModelName: model.ModelNameProgrammingLang,
69 | }
70 | } else if err != nil {
71 | return nil, errors.WithStack(err)
72 | }
73 |
74 | lang.ID = id
75 | lang.Name = param.Name
76 | lang.Feature = param.Feature
77 | lang.UpdatedAt = time.Now().UTC()
78 |
79 | return u.Repo.Update(ctx, lang)
80 | }
81 |
82 | // Delete は、ProgrammingLangを削除する。
83 | func (u *ProgrammingLangUseCase) Delete(ctx context.Context, id int) error {
84 | lang, err := u.Repo.Read(ctx, id)
85 | if lang == nil {
86 | return &model.NoSuchDataError{
87 | ID: id,
88 | ModelName: model.ModelNameProgrammingLang,
89 | }
90 | } else if err != nil {
91 | return errors.WithStack(err)
92 | }
93 |
94 | return u.Repo.Delete(ctx, id)
95 | }
96 |
--------------------------------------------------------------------------------
/server/usecase/program_lang_usecase_test.go:
--------------------------------------------------------------------------------
1 | package usecase
2 |
3 | import (
4 | "context"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
9 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository"
10 | "github.com/SekiguchiKai/clean-architecture-with-go/server/infra/dao/mock"
11 | "github.com/golang/mock/gomock"
12 | )
13 |
14 | func TestNewProgrammingLangUseCase(t *testing.T) {
15 | ctrl := gomock.NewController(t)
16 | defer ctrl.Finish()
17 |
18 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl)
19 |
20 | type args struct {
21 | repo repository.ProgrammingLangRepository
22 | }
23 | tests := []struct {
24 | name string
25 | args args
26 | }{
27 | {
28 | name: "適切な引数を与えると、ProgrammingLangUseCaseが返されること",
29 | args: args{
30 | repo: mock,
31 | },
32 | },
33 | }
34 | for _, tt := range tests {
35 | t.Run(tt.name, func(t *testing.T) {
36 | if got := NewProgrammingLangUseCase(tt.args.repo); got == nil {
37 | t.Errorf("NewProgrammingLangUseCase() = %v, want not nil", got)
38 | }
39 | })
40 | }
41 | }
42 |
43 | func TestProgrammingLangUseCase_List(t *testing.T) {
44 | ctrl := gomock.NewController(t)
45 | defer ctrl.Finish()
46 |
47 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl)
48 |
49 | type fields struct {
50 | Repo repository.ProgrammingLangRepository
51 | }
52 | type args struct {
53 | ctx context.Context
54 | limit int
55 | }
56 |
57 | type wantErr struct {
58 | isErr bool
59 | err error
60 | }
61 |
62 | tests := []struct {
63 | name string
64 | fields fields
65 | args args
66 | want []*model.ProgrammingLang
67 | wantErr wantErr
68 | }{
69 | {
70 | name: "limitを20件にした時に、20件のProgrammingLangを含んだProgrammingLangを返すこと",
71 | fields: fields{
72 | Repo: mock,
73 | },
74 | args: args{
75 | ctx: context.Background(),
76 | limit: 20,
77 | },
78 | want: model.CreateProgrammingLangs(20),
79 | wantErr: wantErr{
80 | isErr:false,
81 | },
82 | },
83 | {
84 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと",
85 | fields: fields{
86 | Repo: mock,
87 | },
88 | args: args{
89 | ctx: context.Background(),
90 | limit: 20,
91 | },
92 | want: nil,
93 | wantErr: wantErr{
94 | isErr:true,
95 | err:&model.DBError{
96 | ModelName: model.ModelNameProgrammingLang,
97 | DBMethod: model.DBMethodList,
98 | Detail: model.TestDBSomeErr,
99 | },
100 | },
101 | },
102 | }
103 | for _, tt := range tests {
104 | t.Run(tt.name, func(t *testing.T) {
105 | u := &ProgrammingLangUseCase{
106 | Repo: tt.fields.Repo,
107 | }
108 |
109 | mock.EXPECT().List(tt.args.ctx, tt.args.limit).Return(tt.want, tt.wantErr.err)
110 |
111 | got, err := u.List(tt.args.ctx, tt.args.limit)
112 | if (err != nil) != tt.wantErr.isErr {
113 | t.Errorf("ProgrammingLangUseCase.List() error = %v, wantErr %v", err, tt.wantErr)
114 | return
115 | }
116 | if !reflect.DeepEqual(got, tt.want) {
117 | t.Errorf("ProgrammingLangUseCase.List() = %v, want %v", got, tt.want)
118 | }
119 |
120 | if !reflect.DeepEqual(err, tt.wantErr.err) {
121 | t.Errorf("ProgrammingLangUseCase.List() = %v, want %v", err, tt.wantErr.err)
122 | }
123 | })
124 | }
125 | }
126 |
127 | func TestProgrammingLangUseCase_Get(t *testing.T) {
128 | ctrl := gomock.NewController(t)
129 | defer ctrl.Finish()
130 |
131 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl)
132 |
133 | wantErrValue := &model.NoSuchDataError{
134 | ID: 1,
135 | ModelName: model.ModelNameProgrammingLang,
136 | }
137 |
138 | type fields struct {
139 | Repo repository.ProgrammingLangRepository
140 | }
141 | type args struct {
142 | ctx context.Context
143 | id int
144 | }
145 |
146 | type wantErr struct {
147 | isErr bool
148 | err error
149 | }
150 |
151 | tests := []struct {
152 | name string
153 | fields fields
154 | args args
155 | want *model.ProgrammingLang
156 | wantErr wantErr
157 | }{
158 | {
159 | name: "IDで指定したProgrammingLang存在する場合、ProgrammingLangを1件返すこと",
160 | fields: fields{
161 | Repo: mock,
162 | },
163 | args: args{
164 | ctx: context.Background(),
165 | id: 1,
166 | },
167 | want: model.CreateProgrammingLangs(1)[0],
168 | wantErr: wantErr{
169 | isErr: false,
170 | err: nil,
171 | },
172 | },
173 | {
174 | name: "IDで指定したProgrammingLang存在しない場合、nilとエラーを返すこと",
175 | fields: fields{
176 | Repo: mock,
177 | },
178 | args: args{
179 | ctx: context.Background(),
180 | id: 1,
181 | },
182 | want: model.CreateProgrammingLangs(1)[0],
183 | wantErr: wantErr{
184 | isErr: true,
185 | err: wantErrValue,
186 | },
187 | },
188 | }
189 | for _, tt := range tests {
190 | t.Run(tt.name, func(t *testing.T) {
191 | u := &ProgrammingLangUseCase{
192 | Repo: tt.fields.Repo,
193 | }
194 |
195 | mock.EXPECT().Read(tt.args.ctx, tt.args.id).Return(tt.want, tt.wantErr.err)
196 |
197 | got, err := u.Get(tt.args.ctx, tt.args.id)
198 | if (err != nil) != tt.wantErr.isErr {
199 | t.Errorf("ProgrammingLangUseCase.Get() error = %v, wantErr %v", err, tt.wantErr)
200 | return
201 | }
202 |
203 | if tt.wantErr.isErr {
204 | if err.Error() != tt.wantErr.err.Error() {
205 | t.Errorf("ProgrammingLangUseCase.Get() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error())
206 | }
207 | }
208 |
209 | if !reflect.DeepEqual(got, tt.want) {
210 | t.Errorf("ProgrammingLangUseCase.Get() = %v, want %v", got, tt.want)
211 | }
212 | })
213 | }
214 | }
215 |
216 | func TestProgrammingLangUseCase_Create(t *testing.T) {
217 | ctrl := gomock.NewController(t)
218 | defer ctrl.Finish()
219 |
220 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl)
221 |
222 | lang := model.CreateProgrammingLangs(1)[0]
223 |
224 | wantErrValue := &model.AlreadyExistError{
225 | ID: 1,
226 | ModelName: model.ModelNameProgrammingLang,
227 | Name: model.CreateProgrammingLangs(1)[0].Name,
228 | }
229 |
230 | type fields struct {
231 | Repo repository.ProgrammingLangRepository
232 | }
233 | type args struct {
234 | ctx context.Context
235 | param *model.ProgrammingLang
236 | }
237 |
238 | type readWant struct {
239 | result *model.ProgrammingLang
240 | err error
241 | }
242 |
243 | type wantErr struct {
244 | isErr bool
245 | err error
246 | }
247 |
248 | tests := []struct {
249 | name string
250 | fields fields
251 | args args
252 | want *model.ProgrammingLang
253 | readWant readWant
254 | wantErr wantErr
255 | }{
256 | {
257 | name: "同一のProgrammingLang存在しない場合、ProgrammingLangを登録すること",
258 | fields: fields{
259 | Repo: mock,
260 | },
261 | args: args{
262 | ctx: context.Background(),
263 | param: lang,
264 | },
265 | want: lang,
266 | readWant: readWant{
267 | result: nil,
268 | err: &model.NoSuchDataError{
269 | Name: lang.Name,
270 | ModelName: model.ModelNameProgrammingLang,
271 | },
272 | },
273 | wantErr: wantErr{
274 | isErr: false,
275 | err: nil,
276 | },
277 | },
278 | {
279 | name: "同一のProgrammingLang存在する場合、nilとエラーを返すこと",
280 | fields: fields{
281 | Repo: mock,
282 | },
283 | args: args{
284 | ctx: context.Background(),
285 | param: lang,
286 | },
287 | want: nil,
288 | readWant: readWant{
289 | result: lang,
290 | err: wantErrValue,
291 | },
292 | wantErr: wantErr{
293 | isErr: true,
294 | err: wantErrValue,
295 | },
296 | },
297 | }
298 | for _, tt := range tests {
299 | t.Run(tt.name, func(t *testing.T) {
300 | u := &ProgrammingLangUseCase{
301 | Repo: tt.fields.Repo,
302 | }
303 |
304 | mock.EXPECT().ReadByName(tt.args.ctx, tt.args.param.Name).Return(tt.readWant.result, tt.readWant.err)
305 |
306 | if !tt.wantErr.isErr {
307 | mock.EXPECT().Create(tt.args.ctx, tt.args.param).Return(tt.want, tt.wantErr.err)
308 | }
309 |
310 | got, err := u.Create(tt.args.ctx, tt.args.param)
311 |
312 | if (err != nil) != tt.wantErr.isErr {
313 | t.Errorf("ProgrammingLangUseCase.Create() error = %v, wantErr %v", err, tt.wantErr)
314 | return
315 | }
316 |
317 | if tt.wantErr.isErr {
318 | if err.Error() != tt.wantErr.err.Error() {
319 | t.Errorf("ProgrammingLangUseCase.Create() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error())
320 | }
321 | }
322 |
323 | if !reflect.DeepEqual(got, tt.want) {
324 | t.Errorf("ProgrammingLangUseCase.Create() = %v, want %v", got, tt.want)
325 | }
326 | })
327 | }
328 | }
329 |
330 | func TestProgrammingLangUseCase_Update(t *testing.T) {
331 | ctrl := gomock.NewController(t)
332 | defer ctrl.Finish()
333 |
334 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl)
335 |
336 | lang := model.CreateProgrammingLangs(1)[0]
337 |
338 | type fields struct {
339 | Repo repository.ProgrammingLangRepository
340 | }
341 | type args struct {
342 | ctx context.Context
343 | id int
344 | param *model.ProgrammingLang
345 | }
346 |
347 | type readWant struct {
348 | result *model.ProgrammingLang
349 | err error
350 | }
351 |
352 | type wantErr struct {
353 | isErr bool
354 | err error
355 | }
356 |
357 | tests := []struct {
358 | name string
359 | fields fields
360 | args args
361 | want *model.ProgrammingLang
362 | readWant readWant
363 | wantErr wantErr
364 | }{
365 | {
366 | name: "同一のProgrammingLang存在する場合、ProgrammingLangを更新すること",
367 | fields: fields{
368 | Repo: mock,
369 | },
370 | args: args{
371 | ctx: context.Background(),
372 | id: 1,
373 | param: lang,
374 | },
375 | want: lang,
376 | readWant: readWant{
377 | result: lang,
378 | err: nil,
379 | },
380 | wantErr: wantErr{
381 | isErr: false,
382 | err: nil,
383 | },
384 | },
385 | {
386 | name: "指定したProgrammingLang存在しない場合、nilとエラーを返すこと",
387 | fields: fields{
388 | Repo: mock,
389 | },
390 | args: args{
391 | ctx: context.Background(),
392 | id: 100,
393 | param: lang,
394 | },
395 | want: nil,
396 | readWant: readWant{
397 | result: nil,
398 | err: &model.DBError{
399 | ModelName: model.ModelNameProgrammingLang,
400 | DBMethod: model.DBMethodRead,
401 | Detail: model.TestDBSomeErr,
402 | },
403 | },
404 | wantErr: wantErr{
405 | isErr: true,
406 | err: &model.NoSuchDataError{
407 | ID: 100,
408 | ModelName: model.ModelNameProgrammingLang,
409 | Name: lang.Name,
410 | },
411 | },
412 | },
413 | }
414 | for _, tt := range tests {
415 | t.Run(tt.name, func(t *testing.T) {
416 | u := &ProgrammingLangUseCase{
417 | Repo: tt.fields.Repo,
418 | }
419 |
420 | mock.EXPECT().Read(tt.args.ctx, tt.args.id).Return(tt.readWant.result, tt.readWant.err)
421 |
422 | if !tt.wantErr.isErr {
423 | mock.EXPECT().Update(tt.args.ctx, tt.args.param).Return(tt.want, tt.wantErr.err)
424 | }
425 |
426 | got, err := u.Update(tt.args.ctx, tt.args.id, tt.args.param)
427 | if (err != nil) != tt.wantErr.isErr {
428 | t.Errorf("ProgrammingLangUseCase.Update() error = %v, wantErr %v", err, tt.wantErr.isErr)
429 | return
430 | }
431 |
432 | if tt.wantErr.isErr {
433 | if err.Error() != tt.wantErr.err.Error() {
434 | t.Errorf("ProgrammingLangUseCase.Update() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error())
435 | }
436 | }
437 |
438 | if !reflect.DeepEqual(got, tt.want) {
439 | t.Errorf("ProgrammingLangUseCase.Update() = %v, want %v", got, tt.want)
440 | }
441 | })
442 | }
443 | }
444 |
445 | func TestProgrammingLangUseCase_Delete(t *testing.T) {
446 | ctrl := gomock.NewController(t)
447 | defer ctrl.Finish()
448 |
449 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl)
450 |
451 | err := &model.NoSuchDataError{
452 | ID: 2,
453 | ModelName: model.ModelNameProgrammingLang,
454 | }
455 |
456 | type fields struct {
457 | Ctx context.Context
458 | Repo repository.ProgrammingLangRepository
459 | }
460 | type args struct {
461 | ctx context.Context
462 | id int
463 | }
464 |
465 | type wantErr struct {
466 | isErr bool
467 | err error
468 | }
469 |
470 | type readWant struct {
471 | result *model.ProgrammingLang
472 | err error
473 | }
474 |
475 | tests := []struct {
476 | name string
477 | fields fields
478 | args args
479 | wantErr wantErr
480 | readWant readWant
481 | }{
482 | {
483 | name: "同一のProgrammingLang存在する場合、ProgrammingLangを更新すること",
484 | fields: fields{
485 | Repo: mock,
486 | },
487 | args: args{
488 | ctx: context.Background(),
489 | id: 1,
490 | },
491 | wantErr: wantErr{
492 | isErr: false,
493 | err: nil,
494 | },
495 | readWant: readWant{
496 | result: model.CreateProgrammingLangs(1)[0],
497 | err: nil,
498 | },
499 | },
500 | {
501 | name: "IDで指定したProgrammingLang存在しない場合、nilとエラーを返すこと",
502 | fields: fields{
503 | Repo: mock,
504 | },
505 | args: args{
506 | ctx: context.Background(),
507 | id: 2,
508 | },
509 | wantErr: wantErr{
510 | isErr: true,
511 | err: err,
512 | },
513 | readWant: readWant{
514 | result: nil,
515 | err: err,
516 | },
517 | },
518 | }
519 | for _, tt := range tests {
520 | t.Run(tt.name, func(t *testing.T) {
521 | u := &ProgrammingLangUseCase{
522 | Repo: tt.fields.Repo,
523 | }
524 |
525 | mock.EXPECT().Read(tt.args.ctx, tt.args.id).Return(tt.readWant.result, tt.readWant.err)
526 |
527 | if tt.readWant.result != nil {
528 | mock.EXPECT().Delete(tt.args.ctx, tt.args.id).Return(tt.wantErr.err)
529 | }
530 |
531 | err := u.Delete(tt.args.ctx, tt.args.id);
532 | if (err != nil) != tt.wantErr.isErr {
533 | t.Errorf("ProgrammingLangUseCase.Delete() error = %v, wantErr %v", err, tt.wantErr.isErr)
534 | }
535 |
536 | if tt.wantErr.isErr {
537 | if err.Error() != tt.wantErr.err.Error() {
538 | t.Errorf("ProgrammingLangUseCase.Delete() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error())
539 | }
540 | }
541 |
542 |
543 | if tt.wantErr.isErr {
544 | if err.Error() != tt.wantErr.err.Error() {
545 | t.Errorf("ProgrammingLangUseCase.Delete() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error())
546 | }
547 | }
548 | })
549 | }
550 | }
551 |
--------------------------------------------------------------------------------
/server/util/checker.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | // IsEmpty は与えられた文字列が空文字かどうかを確認する
4 | func IsEmpty(target string) bool {
5 | return target == ""
6 | }
--------------------------------------------------------------------------------
/server/util/checker_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "testing"
5 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model"
6 | )
7 |
8 | func TestIsEmpty(t *testing.T) {
9 | type args struct {
10 | target string
11 | }
12 | tests := []struct {
13 | name string
14 | args args
15 | want bool
16 | }{
17 | {
18 | name :"空の場合は、trueを返す",
19 | args :args{target:""},
20 | want:true,
21 | },
22 | {
23 | name :"空でない場合は、falseを返す",
24 | args :args{target:model.TestName},
25 | want:false,
26 | },
27 | }
28 | for _, tt := range tests {
29 | t.Run(tt.name, func(t *testing.T) {
30 | if got := IsEmpty(tt.args.target); got != tt.want {
31 | t.Errorf("IsEmpty() = %v, want %v", got, tt.want)
32 | }
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/util/trimer.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import "strings"
4 |
5 | // TrimDoubleQuotes は、ダブルクォーテーションを削除する。
6 | func TrimDoubleQuotes(target string) string {
7 | return strings.Trim(target, "\"")
8 | }
9 |
--------------------------------------------------------------------------------
/server/util/trimer_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import "testing"
4 |
5 | func TestTrimDoubleQuotes(t *testing.T) {
6 | type args struct {
7 | target string
8 | }
9 | tests := []struct {
10 | name string
11 | args args
12 | want string
13 | }{
14 | {
15 | name: "ダブルクォーテーションが1つ存在する場合、ダブルクォーテーションを削除した文字列を返す",
16 | args: args{
17 | target: "\"test",
18 | },
19 | want: "test",
20 | },
21 | {
22 | name: "ダブルクォーテーションが3つ存在する場合、ダブルクォーテーションを削除した文字列を返す",
23 | args: args{
24 | target: "\"\"test\"",
25 | },
26 | want: "test",
27 | },
28 | {
29 | name: "ダブルクォーテーションが存在しない場合、そのままの文字列を返す",
30 | args: args{
31 | target: "test",
32 | },
33 | want: "test",
34 | },
35 | }
36 | for _, tt := range tests {
37 | t.Run(tt.name, func(t *testing.T) {
38 | if got := TrimDoubleQuotes(tt.args.target); got != tt.want {
39 | t.Errorf("TrimDoubleQuotes() = %v, want %v", got, tt.want)
40 | }
41 | })
42 | }
43 | }
44 |
--------------------------------------------------------------------------------