Меня недавно попросили помочь с проектом с целью рекомендовать, как реализовать некоторые дополнительные функции обработки событий. Я просмотрел исходный код и предложил изменить существующую реализацию интерфейса, а также несколько других кусочков; все довольно просто или так я думал. Позже один из разработчиков сказал мне, что он не хотел бы менять этот конкретный фрагмент кода, поскольку он был критически важным, и он не знал, что еще может сломаться, если он будет изменен. И вот когда зазвонили тревожные колокола, это было очень тревожное заявление.
Я верю в то, что у вас всегда должна быть уверенность в том, чтобы разбирать программное обеспечение и изменять, перестраивать и исправлять любую его часть, которая вам нравится. То, что дает вам такую уверенность, это юнит-тесты; если вы испортите изменение, тесты не пройдут, и вы узнаете, что сделали ошибку, и можете что-то исправить. Модульные тесты также означают, что когда вы переходите, другие разработчики также могут быть уверены в том, что разберете ваше программное обеспечение.
Учитывая это утверждение, я должен был выяснить, что происходит, поэтому я сделал обзор программного обеспечения и обнаружил тот факт, что было не так много юнит-тестов, фактически в некоторых областях их вообще не было… но они имели ряд интеграционных тестов. Странный. Что происходило?
Были ли они просто плохими программистами? Разве они не понимают, что вы можете писать модульные тесты в Go? Были ли разработчики настолько наивны, что думали, что им не нужны тесты?
Рассматривая исходный код немного глубже, ответ на эти вопросы был:
Нет, они не были ,
да, они сделали и
нет, они не были .
Как я сказал в начале этого блога, меня попросили порекомендовать добавить некоторые функции обработки событий, и это ключ к проблеме. Типичный обработчик событий имеет следующий программный поток:
- Получить сообщение
- Разобрать сообщение
- Используйте это, чтобы прочитать что-то из вашей базы данных
- Заниматься бизнес-логикой
- Сделайте еще немного бизнес-логики
- Напишите что-нибудь в базу данных
- Есть некоторая логика с результатом записи
- Напишите что-нибудь еще в базу данных
- Убирать
… И их обработчик событий был довольно типичным. Проблема заключалась в том, что их код обращался к базе данных, и не было никаких модульных тестов, потому что они не знали, как имитировать соединения с базой данных. Если вы пришли из Java-опыта, как я, вы, вероятно, будете знакомы с различными доступными фреймворками: Mockito, Easymock, Powermock и т. Д. В мире Go популярные вездесущие фреймворки не совсем доступны и, хотя они делают многие программисты на Go считают, что они не нужны. Это не потому, что они ленивы (я надеюсь), а потому, что если вы знаете, как, в Go легко что-то издеваться без одного.
Этот блог демонстрирует макетирование соединения с базой данных в обработчике событий Go, и для этого первое, что нам нужно, это некоторый код доступа к базе данных.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// The real DB Connection details. type RealConnection struct { host string // The DB host to connect to port int32 // The port URL string // DB access URL / connection string driver *SomeDriver // a pointer to the underlying 3rd party database driver // other stuff may go here } // This is any old DB read func, you'll have lots of these in your application - probably. func (r *RealConnection) ReadSomething(arg0, arg1 string) ([]string, error) { fmt.Printf( "This is the real database driver - read args: %s -- %s" ,arg0,arg1) return []string{}, nil } // This is any old DB insert/ update function. func (r * RealConnection) WriteSomething(arg0 []string) error { fmt.Printf( "This is the real database driver - write args: %v" ,arg0) return nil } // Group together the methods in one or more interfaces, gathering them along functionality lines and // keeping the interface small. type SomeFunctionalityGroup interface { ReadSomething(arg0, arg1 string) ([]string, error) WriteSomething(arg0 []string) error } |
Здесь я предполагаю, что если вы используете сторонний пакет базы данных Go, то вы оберните этот пакет в свой собственный код доступа к базе данных. Это предотвратит утечку стороннего кода по всему вашему приложению и обеспечит уровень базы данных вашего стандартного дизайна N-уровня.
Первое, на что следует обратить внимание, это то, что код dbaccess
моделирует соединение с базой данных в структуре RealConnection
. В реальном мире это может содержать такие вещи, как имя хоста базы данных и другие детали соединения.
Следующая вещь, в которой нуждается настоящий пакет базы данных, — это реальные методы доступа к базе данных. Здесь я использую RealConnection
в качестве приемника.
Последнее, что следует отметить, это то, что я собрал связанные методы доступа к базе данных в интерфейс, подчиняясь рекомендациям Go по выбору небольших интерфейсов. Это интерфейс SomeFunctionalityGroup
.
После создания базы данных нам понадобится следующий обработчик событий.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// The event handler struct. Models event attributes type EventHandler struct { name string // The name of the event actor dbaccess.SomeFunctionalityGroup // The interface for our dbaccess fucntions } // This creates a event handler instance, using whatever name an actor are passed in. func NewEventHandler(actor dbaccess.SomeFunctionalityGroup, name string) EventHandler { return EventHandler{ name: name, actor: actor, } } // This is a sample event handler - it reads from the DB does some imaginary business logic and writes the results back // to the DB. func (eh *EventHandler) HandleSomeEvent(action string) error { fmt.Printf( "Handling event: %s\n" , action) value, err := eh.actor.ReadSomething(action, "arg1" ) if err != nil { fmt.Printf( "Use the logger to log your error here. The read error is: %+v\n" , err) return err } // Do some business logic here if len(value) == 2 && value[ 0 ] == "Hello" { value[ 1 ] = "World" } // Now write the result back to the database err = eh.actor.WriteSomething(value) if err != nil { fmt.Printf( "Use the logger to log your error here. The write error is: %+v\n" , err) } return err } |
Я создал структуру EventHandler
и функцию NewEventHandler
потому что я предполагаю, что код будет обрабатывать различное количество потоков событий и что нам понадобится обработчик для каждого из них. Ключевым моментом, который следует отметить, является то, что моя структура EventHandler
имеет ссылку на интерфейс SomeFunctionalityGroup
в форме атрибута actor
. Если это был один обработчик событий, то можно также использовать простую глобальную функцию, если она имеет доступ к реализации SomeFunctionalityGroup
.
1
2
3
4
|
type EventHandler struct { name string // The name of the event actor dbaccess.SomeFunctionalityGroup // The interface for our dbaccess fucntions } |
Сам обработчик событий читает базу данных, выполняет некоторую бизнес-логику и записывает в базу данных, и именно эти операции чтения и записи нам нужно MockConnection
, и мы делаем это путем создания структуры MockConnection
1
2
3
4
|
type MockConnection struct { fail bool // Set this to true to mimic a DB read / write failure } |
Это может содержать атрибуты, которые могут заставить ваш модульный тест завершить как счастливые, так и неудачные программные потоки. В этом простом примере мы имеем логическое значение fail
.
Следующим шагом является реализация интерфейса SomeFunctionalityGroup
с использованием MockConnection
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// This is the mock database read function func (r *MockConnection) ReadSomething(arg0, arg1 string) ([]string, error) { fmt.Printf( "This is the MOCK database driver - read args: %s -- %s\n" ,arg0,arg1) if r.fail { fmt.Println( "Whoops - there's been a database write error" ) return []string{}, dummyError } return []string{ "Hello" , "" }, nil } // This is mock database write function func (r * MockConnection) WriteSomething(arg0 []string) error { fmt.Printf( "This is the MOCK database driver - write args: %v\n" ,arg0) if r.fail { fmt.Println( "Whoops - there's been a database write error" ) return dummyError } return nil } |
С этого момента все просто, и мы можем создать ряд юнит-тестов:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// Test calling the event handler with a dummy database connection. func TestEventHandlerDB_happy_flow(t *testing.T) { testCon := MockConnection{ fail: false , } eh := NewEventHandler(&testCon, "Happy" ) err := eh.HandleSomeEvent( "Action" ) if err != nil { t.Errorf( "Failed - with error: %+v\n" , err) } } // Test calling the event handler with a dummy database connection, for the failure flow. func TestEventHandlerDB_fail_flow(t *testing.T) { testCon := MockConnection{ fail: true , } eh := NewEventHandler(&testCon, "Fail" ) err := eh.HandleSomeEvent( "Action 2" ) if err == nil { t.Errorf( "Failed - with error: %+v\n" , err) } } |
Все тесты, которые нужно сделать, — это создать MockConnection
а затем создать экземпляр EventHandler
перед вызовом HandleSomeEvent(...)
и проверкой его результата.
Эта идея действительно гибкая: вы можете добавить проверки, которые, например, проверяют, что методы вызываются с правильными аргументами заданное количество раз, или вы также можете настроить
MockConnection
, так что она предоставляет возвращаемые значения для ваших фиктивных ReadSomething(...)
и WriteSomething(...)
базы данных. Более полная структура может выглядеть так:
1
2
3
4
5
6
7
8
|
// A MockConnection mimics a real database connection - but allows us to mock the connection and follow happy and fail code paths type MockConnection struct { readFail bool // Set this to true to mimic a DB read failure writeFail bool // Set this to true to mimic a DB read failure readReturn []string // The return from the ReadSomething(...) method readCallCount int32 // The number of database reads expected writeError error // A specific write error type expected. } |
Настроить что-то подобное очень просто, если вы знаете, как и намного проще, чем настроить эквивалентные интеграционные тесты. Для меня правила создания надежных, тестируемых приложений одинаковы, независимо от того, какой язык вы используете. Если вы интересуетесь тестированием в целом, существует множество других блогов на эту тему, включая
ПЕРВЫЙ Акроним , почему вы должны написать модульные тесты и многое другое.
Исходный код, используемый в этом блоге, можно найти на Github по адресу:
https://github.com/roghughe/gsamples/tree/master/mockingsample
Опубликовано на Java Code Geeks с разрешения Роджера Хьюза, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Насмешка в Го
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |