![[Learn Go with Tests] 구조체, 메서드 & 인터페이스](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FzE6Gb%2FbtsIDOI0HaR%2FAAAAAAAAAAAAAAAAAAAAAEYBlh4xIfuuoNVMuyWo1MmfCYVIt2sj6w2O-7DfRa--%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1753973999%26allow_ip%3D%26allow_referer%3D%26signature%3DHsl7LhggLGh1iE%252B7PDrIKRy1GHo%253D)
해당 포스팅은 Learn Go with Tests Gitbook을 따라 실습한 내용을 정리한 문서입니다.
사각형의 둘레를 계산하는 코드
테스트 코드
func TestPerimeter(t *testing.T){
got := Perimeter(10.0, 10.0)
want := 40.0
if got != want{
t.Errorf("got %.2f want %.2f", got, want)
}
}
⇒ 가로와 세로를 입력받고 둘레를 계산하는 프로그램의 테스트 코드이다.
- 새로운 문자열 형식 → %.2f
- f는 float64이고, .2는 소수점 두자리 출력을 의미한다.
실행 코드
func Perimeter(width float64, height float64) float64 {
return 2 * (width + height)
}
면적을 계산하는 코드
테스트 코드
func TestArea(t *testing.T){
got := Area(12.0, 6.0)
want := 72.0
if got != want{
t.Errorf("got %.2f want %.2f", got, want)
}
}
실행 코드
func Area(width float64, height float64) float64 {
return width * height
}
Struct를 도입해 리팩토링하기
기존의 코드는 제대로 작동하지만, 직사각형에 대한 명시적인 내용이 존재하지 않는다. 삼각형을 해당 함수에 사용할 경우 또한 오류가 발생할 수 있다.
⇒ Rectangle 이라는 자신만의 type을 정의해 캡슐화를 하도록 하자.
type Rectange struct{
Width float64
Height float64
}
이제 Float64가 아닌 Rectange로 코드를 리팩토링하자.
테스트 코드 리팩토링
func TestPerimeter(t *testing.T) {
rectangle := Rectange{10.0, 10.0}
got := Perimeter(rectangle)
want := 40.0
if got != want {
t.Errorf("got %.2f want %.2f", got, want)
}
}
func TestArea(t *testing.T) {
rectangle := Rectange{10.0, 10.0}
got := Area(rectangle)
want := 72.0
if got != want {
t.Errorf("got %.2f want %.2f", got, want)
}
}
기존 코드 리팩토링
type Rectange struct {
Width float64
Height float64
}
func Perimeter(rectangle Rectange) float64 {
return 2 * (rectangle.Width + rectangle.Height)
}
func Area(rectangle Rectange) float64 {
return rectangle.Width * rectangle.Height
}
원에 대한 구조체 추가하기
테스트 코드 작성하기
func TestArea(t *testing.T) {
t.Run("rectangles", func(t *testing.T) {
rectangle := Rectangle{12, 6}
got := Area(rectangle)
want := 72.0
if got != want {
t.Errorf("got %g want %g", got, want)
}
})
t.Run("circles", func(t *testing.T) {
circle := Circle{10}
got := Area(circle)
want := 314.1592653589793
if got != want {
t.Errorf("got %g want %g", got, want)
}
})
}
⇒ 여기서는 %f가 아닌 %g를 사용한다. %f를 사용하게 되면 정확한 소수의 값을 표현하는 것이 가능하다.
ex)
fmt.Printf("%g %g\\n", 1234567.1234567, 1.2) // 1.2345671234567e+06 1.2
기존 코드 작성하기
go에서는 오버로딩을 지원하지 않아 다음과 같은 코드 작성은 불가능하다!
func Area(circle Circle) float64 { ... }
func Area(rectangle Rectangle) float64 { ... }
⇒ 이건 안됨 !!!
- 동일한 함수의 다른 패키지 선언 → 여기선 과하다 다음시간에 배워보자
- method를 정의하자
메서드 도입하기
메서드란 무엇인가 ?
- t.Errorf를 사용할 때 우리는 t (testing.T)의 인스턴스에서 Errorf 메서드를 사용했다
- 수신기가 있는 함수를 메서드라고 한다
- 메서드 선언 → 식별자를 메서드에 바인딩하고 수신기의 기본 유형과 연결한다.
⇒ 함수와 유사하지만, 특정 유형의 인스턴스에서 메서드를 호출해야 한다.
테스트 코드 작성하기
func TestArea(t *testing.T) {
t.Run("rectangles", func(t *testing.T) {
rectangle := Rectangle{12, 6}
got := rectangle.Area(rectangle)
want := 72.0
if got != want {
t.Errorf("got %g want %g", got, want)
}
})
t.Run("circles", func(t *testing.T) {
circle := Circle{10}
got := circle.Area(circle)
want := 314.1592653589793
if got != want {
t.Errorf("got %g want %g", got, want)
}
})
}
circle.Area 와 같이 인스턴스에서 메서드를 호출하는 방식으로 테스트 코드를 변경할 수 있다.
기존 코드 작성하기
func (r Rectange) Area(retangle Rectange) float64 {
return r.Width * r.Height
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
메서드에서는 수신기가 존재한다. func (receiverName ReceiverType) MethodName(args)의 구문으로 작성하도록 하자.
- 메서드가 해당 유형의 변수 호출 → receiverName 변수를 통해 해당 데이터에 대한 참조를 얻는다.
- 즉, receivername을 통해 수신기로 접근한다.
- Go의 관례 → 수신자 변수를 유형의 첫번째 문자로 지정하자
- ex) Rectange은 r로, Circle은 c로 지정하게 된다.
테스트 코드 리팩토링하기
도형이 아닌 것을 전달할 때 컴파일 할 수 없어야 하므로, 해당 기능을 checkArea 함수를 통해 구현하고자 한다.
- interface를 통해 의도를 코드화 하는 것이 가능하다.
- 여러 유형과 함게 사용할 수 있는 함ㅅ수를 만들어 안정성을 유지하고 세분화 할 수 있다.
func TestArea(t *testing.T) {
checkArea := func(t testing.TB, shape Shape, want float64) {
t.Helper()
got := shape.Area()
if got != want {
t.Errorf("got %g want %g", got, want)
}
}
t.Run("rectangles", func(t *testing.T) {
rectangle := Rectangle{12, 6}
checkArea(t, rectangle, 72.0)
})
t.Run("circles", func(t *testing.T) {
circle := Circle{10}
checkArea(t, circle, 314.1592653589793)
})
}
어떤것이 도형이 되어야 하는지는 인터페이스 선언을 사용하는 Shape가 무엇인지 정의하면 된다.
type Shape interface {
Area() float64
}
⇒ Rectangle과 circle을 만든 것 처럼 새로운 interface의 타입을 만들고, 이번에는 struct가 아닌 interface로 선언하면 된다.
- Rectangle과 Circle은 Area 메서드를 호출하여 float64를 반환하므로 Shape 인터페이스를 만족히키게 된다.
⇒ 즉, 전달하는 유형이 인터페이스가 요청하는 유형과 일치하면 된다.
추가 리팩터링 : 테이블 기반 테스트
func TestArea(t *testing.T) {
areaTests := []struct {
shape Shape
want float64
}{
{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
}
for _, tt := range areaTests {
got := tt.shape.Area()
if got != tt.want {
t.Errorf("got %g want %g", got, tt.want)
}
}
}
- shape와 want라는 두개의 필드가 있는 struct를 선언한다.
- []struct{} 형태 → struct 타입을 여러개 가질 수 있는 것
- 인터페이스의 구현 테스트 → 함수에 전달된 데이터에 테스트가 필요한 여러 요구사항이 있는지를 접목한 항목이다.
삼각형 추가해서 테스트하기
func TestArea(t *testing.T) {
areaTests := []struct {
shape Shape
want float64
}{
{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
{Triangle{12, 6}, 36.0},
}
for _, tt := range areaTests {
got := tt.shape.Area()
if got != tt.want {
t.Errorf("got %g want %g", got, tt.want)
}
}
}
⇒ 위와 같이 삼각형 포맷을 추가할 수 있다.
type Triangle struct {
Base float64
Height float64
}
구조체만 만들고 테스트를 진행하면 Area() 메서드에 삼각형이 없기 때문에 오류가 나온다. 따라서 메서드를 추가해 보자.
func (t Triangle) Area() float64 {
return (t.Base * t.Height) * 0.5
}
이렇게 구현을 하면 통과가 되는 것을 볼 수 있다.
리팩토링 하기 : 테스트 코드의 의미가 분명하도록
{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
{Triangle{12, 6}, 36.0},
현재의 이 코드는 무엇을 의미하는지 의미가 불분명하다. 따라서, 쉽게 이해를 할 수 있도록 코드를 변경해보자.
{shape: Rectangle{Width: 12, Height: 6}, want: 72.0},
{shape: Circle{Radius: 10}, want: 314.1592653589793},
{shape: Triangle{Base: 12, Height: 6}, want: 36.0},
이렇게 변경하여 선택적으로 필드 이름을 지정해줄 수 있다.
'Language > Go' 카테고리의 다른 글
[Learn Go with Tests] 맵 (0) | 2024.07.24 |
---|---|
[Learn Go with Tests] 포인터 & 에러 (1) | 2024.07.24 |
[Learn Go with Tests] 슬라이스 및 배열 (0) | 2024.07.18 |
[Learn Go with Tests] Iteration (0) | 2024.07.18 |
[Learn Go with Tests] Integer (1) | 2024.07.18 |
보안 전공 개발자지만 대학로에서 살고 싶어요
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!