league
를 추가하는 과정을 계속 반복해왔다. 그 과정에서 JSON을 다루는 법, 타입을 임베딩하는 법 그리고 라우팅하는 법을 배울 수 있었다.league
엔드포인트가 이긴 횟수를 기준으로 정렬한 선수들을 반환해야 하는 것을 이해하지 못하는 것이 만족스러워하지 않는다!PlayerStore
를 추상화했기 때문에 만약 우리의 환경이 변하고 더 이상 적절하지 않다면 간단히 다른 무언가로 간단히 변경할 수 있다.InMemoryPlayerStore
를 유지할 것이기 때문에 통합 테스트들은 우리가 새로운 스토어를 개발하는 동안에도 계속 통과할 것이다. 우리는 새로운 구현이 통합 테스트를 충분히 통과할 것이라는 확신을 가지게 됬을 때 이를 교체하고 InMemoryPlayerStore
를 삭제할 것이다.io.Reader
), 쓰는(io.Writer
) 표준 라이브러리들의 인터페이스와 실제 파일들을 사용하지 않고 이런 기능들을 테스트하기 위해 표준 라이브러리를 사용하는 법에 익숙해져야 한다.PlayStore
를 구현해야 한다. 그리고 스토어가 우리가 구현해야 하는 메서드를 호출할 수 있도록 하는 테스트를 작성해야 한다. GetLeague
부터 시작해보자.FileSystemPlayerStore
가 데이터를 읽을 수 있도록 하는 Reader
를 반환하는 strings.NewReader
를 사용중이다. main
에 파일을 추가할 것이고, 이 파일 또한 Reader
이다.FileSystemPlayerStore
를 정의한다.Reader
를 전달했지만 입력을 받을 준비가 되지 않았고, GetLeague
가 아직 정의되어 있지 않기 때문에 컴파일 에러가 발생한다.league.go
라는 새로운 파일을 생성해 안에 넣는다.server_test.go
안에 있는 getLeagueFromResponse
테스트 헬퍼에서 이를 호출한다.io.Reader
가 어떻게 정의되어 있는지 다시 생각해보자.Read
를 두 번 시도한다면 어떻게 될까?Reader
가 끝에 다다랐을 때 더 이상 읽을 것이 없다는 것이다. 우리는 처음으로 돌아가라고 말해줄 방법이 필요하다.FileSystemPlayerStore
을 이 인터페이스로 바꿀 수 있을까?string.NewReader
도 ReadSeeker
를 구현하고 있어서 더 이상 변경할 필요가 없다.GetPlayerScore
를 구현할 것이다.RecorddWin
으로 점수들을 기록해야 한다.Writer
를 사용했지만, 우리는 이미 우리만의 ReadSeeker
를 가지고 있다. 잠재적으로 우리는 2개의 의존성을 가질 수 있지만, 표준 라이브러리는 이미 파일에 필요한 모든 작업을 수행할 수 있는 ReadWriteSeeker
인터페이스를 가지고 있다.strings.Reader
가 ReadWriteSeeker
를 구현하지 못한다는 것은 그리 놀라운 일이 아니다. 그렇다면 무엇을 해야할까?*os.File
은 ReadWriteSeeker
를 구현한다. Create a temporary file for each test. *os.File
implements ReadWriteSeeker
. 이 방법의 장점은 더 통합 테스트에 가까워진다는 것이다. 우리는 실제 파일 시스템에서 읽고 쓰고 있기 때문에 더 높은 수준의 신뢰성을 얻을 수 있습니다. 단점은 단위 테스트가 더 빠르고 일반적으로 간단하기 때문에 더 선호된다는 것이다. 또한 임시 파일들을 만들어내고 테스트 이후에 파일들이 지워졌는지 확인하기 위한 일들을 더 해야만한다.os.File
을 strings.Reader
로 바꿔서 테스트가 컴파일 될 수 있도록 해야한다."db"
값은 만들어질 임의 파일 이름에 붙는 접두사입니다. 이렇게 함으로써 다른 파일들과 우연히 충돌하는 것을 방지합니다.ReadWriteSeeker
(파일) 뿐만 아니라 함수 또한 반환하고 있다는 것을 알고 있어야 합니다. 테스트가 끝나면 파일이 삭제되어야 한다는 것을 확실히 해야합니다. 에러가 발생하기 쉽고 Reader 무관심하기 때문에 테스트에 파일들의 세부사항들을 유출하고 싶지 않다. removeFile
함수를 반환함으로써, 헬퍼 안의 세부사항들을 관리할 수 있고 모든 호출자는 defer cleanDatabase()
만 실행하기만 하면 된다../file_system_store_test.go:67:8: store.RecordWin undefined (type FileSystemPlayerStore has no field or method RecordWin)
player.Wins++
가 아닌 league[i].Wins++
실행하는지 스스로 되묻고 있을 수도 있다.범위
를 지정하면 루프의 현재 인덱스(우리의 경우 i
)와 이 인덱스의 요소의 복사본 을 반환받는다. 복사본 Wins
값의 변경은 우리가 반복중인 league
슬라이스에 아무런 영향을 주지 않는다. 때문에, league[i]
를 이용해 실제 값에 대한 참조를 얻어오고 그 값을 변경해야 한다.GetPlayerScore
와 RecordWin
에서 플레이어를 이름으로 찾기 위해 []Player
를 반복시킨다.FileSystemStore
내부에서 리팩터링할 수 있겠지만, 나에게는 이 코드가 새로운 타입으로 만들 수 있는 유용한 코드가 될 것이라는 느낌이 든다. 지금까지의 "League" 작업은 []Player
와 함께 였지만, 새로운 타입인 League
를 생성할 수 있다. 이렇게 하면 다른 개발자들이 이해하기도 쉬울 것이고 우리가 사용할 수 있도록 새로운 메서드를 붙일 수도 있을 것이다.league.go
안에 다음 코드를 추가한다.League
를 가진 누구나 쉽게 선수를 주어진 선수를 찾을 수 있습니다.PlayerStore
인터페이스가 []Player
가 아닌 League
를 반환하도록 변경하자. 다시 테스트를 실행하면 인터페이스를 변경했기 때문에 컴파일 에러를 얻을 것이다. 하지만 매우 고치기 쉽다; 그냥 반환 타입을 []Player
에서 League
로 변경해라.file_system_store
안에 있는 메서드를 간단하게 할 수 있다.League
와 관련된 다른 유용한 기능들을 리팩터링 하는 방법에 대해서 알 수 있었다.Find
가 선수를 찾을 수 없을 때 nil
을 반환하는 시나리오를 처리하면 된다.Store
를 통합 테스트에서 사용해볼 수 있다. 이를 통해 소프트웨어가 잘 동작한다는 확신을 얻을 수 있고, 중복인 InMemoryPlayerStore
를 삭제할 수 있다.TestRecordingWinsAndRetrievingThem
안의 오래된 스토어를 바꾼다.InMemoryPlayerStore
. main.go
는 컴파일 문제가 생겼을 것이다. 컴파일 문제가 생겼다는 것은 이제 "실제" 코드에서 새로운 스토어를 사용해야 한다는 것을 말해준다.os.OpenFile
의 두번째 인자값으로 파일을 열 수 있는 권한을 정의할 수 있다. O_RDWR
는 읽고 쓸 수 있다는 것을 의미하고, 그리고 os.O_CREATE
는 존재하지 않는 파일을 생성할 수 있다는 것을 의미한다.GetLeague()
또는 GetPlayerScore()
를 호출하면 전체 파일을 읽어와 JSON으로 파싱한다. 하지만 FileSystemStore
가 league 전체의 상태를 책임을 가지기 때문에 이렇게 할 필요가 없다; 프로그램이 시작할 때와 데이터가 바뀌어서 파일을 업데이트해야할 때만 파일을 읽어야만 한다.FileSystemStore
to be used on the reads instead.f.league
를 대신에 사용하면 된다.FileSystemPlayerStore
를 초기화하는 것에 대한 불평할 것이다. 그러므로 새로운 생성자를 호출하는 것으로 바꾸기만 하면 된다.RecordWin
할 때, 파일의 처음으로 Seek
하기 위해 돌아가고 새로운 데이터를 쓴다-하지만 새로운 데이터가 이 전에 있었던 것들보다 더 작다면 어떻게 될까?Tape
이라고 부를 것이다. 다음과 같은 새로운 파일을 생성한다:Seek
를 캡슐화했으니, Write
를 구현하고 있다는 것에 주의해야한다. FileSystemStore
가 Writer
를 대신에 참조로 가질 수 있다는 것을 의미한다.Tape
를 사용하도록 생성자를 업데이트한다.RecordWin
호출로부터 Seek
를 제거함으로써 우리가 원했던 놀라운 성과를 얻을 수 있다. 맞다, 그렇게 크게 느껴지지는 않는다, 하지만 이것은 쵯환 우리가 만약 다른 종류의 쓰기를 할 경우 우리가 원하는대로 동작하는 우리만의 Write
에 의존할 수 있다는 것이다. 추가로 이제부터 잠재적으로 문제가 있는 코드를 각각 테스트할 수 있고 수정할 수 있게 되었다.tape
을 이용해 이 파일에 쓰고, 파일에 무엇이 있는지 전체를 다시 읽어온다. tape_test.go
내부를 살펴보자:os.File
은 truncate 함수를 가지고 있고 이를 활용하면 효과적으로 파일을 비울 수 있다. 우리는 원하는 것을 얻기 위해서는 이 함수를 호출할 수 있어야 한다.tape
을 변경한다:io.ReadWriteSeeker
가 예상되지만 *os.File
을 보내고 있는 많은 곳들에서 실패할 것이다. 지금까지는 이런 문제들을 스스로 수정할 수 있었지만, 만약 막힌다면 소스코드를 확인해라.TestTape_Write
테스트는 통과될 것이다!RecordWin
에는 json.NewEncoder(f.database).Encode(f.league)
라는 코드 라인이 있다.Encoder
의 참조를 타입에 저장하고 생성자 내에서 초기화한다.RecordWin
에서 이를 사용한다.PlayStore
를 유닛 테스트하기 위한 가장 쉬운 방법인 io.Reader
를 사용해서 코드를 만들기 시작했다. 코드를 개발해나가며 io.ReaderWriter
에서 io.ReadWriteSeeker
로 옮겨갔고, *os.File
와는 별개로 실제로 구현된 것이 표준 라이브러리 안에는 없다는 것을 알게 되었다. 우리는 직접 만들거나 오픈소스를 사용하는 것으로 결정을 내렸을 수도 있었지만 테스트를 위한 임시 파일을 만드는 것이 실용적이라고 느꼈다.*os.File
에도 있는 Truncate
가 필요하다. 이런 요구사항들을 만족시키는 자체 인터페이스를 만들기 위한 옵션이었을 것이다.*os.File
외의 다른 타입을 가지는 것이 비현실적이기 때문에 인터페이스의 다형성이 필요하지 않다는 것을 명심해야 한다.FileSystemStore.go
로 돌아가보면 생성자 안에는 league, _ := NewLeague(f.database)
가 있다.NewLeague
는 제공받는 io.Reader
로부터 league를 파싱하지 못하는 경우 에러를 반환할 수 있다.