본문 바로가기
Language/Go

Go - array, slice, test coverage

by ocwokocw 2021. 12. 4.

참조: https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices

- 개요

Go 에서 하나의 데이터 타입을 여러 개 저장할 수 있는 array 와 slice 에 대하여 알아본다. 또한 test coverage 와 array, slice 를 비교하는 reflect.DeepEqual 에 대해서도 알아본다.

- Array & Slice Example

array 의 수를 모두 더하면 얼마인지 구하는 기능을 구현해보자.
 
기능을 완성하고 나면 아래와 같이 array 를 인자로 받아서 단일값으로 반환하는 Sum 과 같은 함수를 호출할 것이다. test 값은 1,2,3,4,5 의 합이므로 기대값은 15이다.  
 
package main

import "testing"

func TestSum(t *testing.T) {

    numbers := [5]int{1, 2, 3, 4, 5}

    got := Sum(numbers)
    want := 15

    if got != want {
        t.Errorf("got %d want %d given, %v", got, want, numbers)
    }
}
 
go test 를 실행하면 Sum 을 구현 하라고 오류가 날 것이다. 아래처럼 컴파일만 통과하도록 한 후 go test 로 기대값과 다를 때 오류를 잘 반환하는지 확인한다.
 
package main

func Sum(numbers [5]int) int {
    return 0
}
 
이제 Test 를 통과하도록 내용을 제대로 구현해보자.
 
func Sum(numbers [5]int) int {
    sum := 0
    for i := 0; i < 5; i++ {
        sum += numbers[i]
    }
    return sum
}
 
이제 go test 를 다시 수행해보면 결과가 올바르게 나올것이다. 이제 test 를 통과했으니 리팩토링할 사항이 있는지 살펴보자.
 
func Sum(numbers [5]int) int {
    sum := 0
    for _, number := range numbers {
        sum += number
    }
    return sum
}
 
go 에서는 array 를 탐색할 수 있는 range 구문을 지원한다. range 는 2개 값을 반환하는데 (index, value) 형태를 반환한다. go 에서는 여러 개의 값을 반환할 수 있다. 우리는 array 의 값을 더하는데에만 관심이 있고 index 는 사용하지 않으므로 index 반환부는 _(blank identifier) 로 무시하면 된다.
 
위의 Sum 함수는 [5]int 형을 인자로 받고 있는데 만약 4개의 값만 더하고 싶어서 [4]int 형을 인자로 넘기면 컴파일이 되지 않는다. 유연성이 너무 떨어지므로 크기에 관계없이 값을 받아 합을 구하도록 함수를 수정해보자.
 
여태까지 Test 작성 -> 구현 -> 리팩토링을 수행하였다. 리팩토링중 문제가 있음을 인지하고 크기에 관계없이 값을 받을 수 있도록 하기 위해 코드를 변경하려고 한다. 이때 크기가 변경 되어도 코드가 잘 돌아가도록 Test 코드를 작성하는 단계로 회귀한다.
 
func TestSum(t *testing.T) {

    t.Run("collection of 5 numbers", func(t *testing.T) {
        numbers := [5]int{1, 2, 3, 4, 5}

        got := Sum(numbers)
        want := 15

        if got != want {
            t.Errorf("got %d want %d given, %v", got, want, numbers)
        }
    })

    t.Run("collection of any size", func(t *testing.T) {
        numbers := []int{1, 2, 3}

        got := Sum(numbers)
        want := 6

        if got != want {
            t.Errorf("got %d want %d given, %v", got, want, numbers)
        }
    })

}
 
위의 코드에서 []int{1,2,3}은 크기를 지정하지 않은 collection 인데 Go 에서는 이를 slice 라고 한다. 컴파일이 되도록 Sum 함수를 수정해보자.
 
수정시에 2 가지 방법증 하나를 택해야 하는데 첫번째는 기존 함수를 고치는것이다. [5]int 형 인자를 []int 로 고치면 된다. 하지만 만약 이 함수를 다른 누군가가 사용한다면 해당 사용자의 코드는 작동하지 않게 된다. 2번째 방법은 새로운 함수를 만드는것이다.
 
현재는 우리만 테스트 코드를 작성하고 있으므로 기존 함수의 인자 형을 변경하도록 하자.
 
func Sum(numbers []int) int {
    sum := 0
    for _, number := range numbers {
        sum += number
    }
    return sum
}
 
위와 같이 고쳐도 Test 코드 컴파일이 되지 않을 것이다.  [5]int 와 []int는 다르기 때문인데 Test 코드를 slice 형으로 수정해준다. 
 
func TestSum(t *testing.T) {

    t.Run("collection of 5 numbers", func(t *testing.T) {
        numbers := []int{1, 2, 3, 4, 5}

        got := Sum(numbers)
        want := 15

        if got != want {
            t.Errorf("got %d want %d given, %v", got, want, numbers)
        }
    })
 
여기서 한 가지 중요한점이 있는데 테스트 코드를 작성할 때, 테스트를 많이 하려고 작성하는게 아니라 코드가 돌아갈것이라는 자신감과 안정감을 위해 작성한다는 점이다. 현재 5개의 합과 3개의 합을 테스트하는 코드가 있다. 만약 [5]int array 형과 []int slice 형을 둘 다 테스트를 한다면 2 개의 테스트가 의미가 있지만 둘 다 slice 형이므로 하나가 동작하면 slice 형은 동작할것이라는 생각으로 하나를 없애야 한다. 작성한 테스트도 결국 유지보수를 해야하는 비용이라는 측면으로 접근해야 한다.

- Coverage tool

Go 는 자체적으로 coverage tool 을 제공해주는데 테스트에 의해 수행되지 않은 영역을 식별하는데 도움을 준다. go test -cover 를 수행해보자.
 
slices % go test -cover
PASS
coverage: 100.0% of statements
 
slice 의 크기가 0 일 때, 0을 반환하는 if 문을 Sum 함수에 추가하면 커버리지가 100%가 안되는것을 확인할 수 있다.

- 새로운 요구사항

slice 들을 인자로 받아 각 slice 의 합을 반환하는 기능을 추가해보자. 예를 들어 SumAll([]int{1,2}, []int{0,9}) 와 같이 넘기면 []int{3, 9} 를 반환한다. Test 코드를 먼저 작성하자.
 
func TestSumAll(t *testing.T) {

    got := SumAll([]int{1, 2}, []int{0, 9})
    want := []int{3, 9}

    if got != want {
        t.Errorf("got %v want %v", got, want)
    }
}
 
go test 를 수행하면 SumAll 함수를 구현하라고 나올것이다. 
 
func SumAll(numbersToSum ...[]int) (sums []int) {
    return
}
 
위와 같이 기본적인 구현만 하면 SumAll 에서는 문제가 없지만 컴파일 문제가 발생하는곳이 하나가 더 있다. got 과 want 를 비교하는 부분인데, Go 는 slice 비교시 equals 연산자 비교를 허용하지 않는다. reflect.DeepEqual 을 이용해야 한다.
 
func TestSumAll(t *testing.T) {

    got := SumAll([]int{1, 2}, []int{0, 9})
    want := []int{3, 9}

    if !reflect.DeepEqual(got, want) {
        t.Errorf("got %v want %v", got, want)
    }
}
 
이제 테스트가 pass 하도록 SumAll 내부를 구현해보자.
 
func SumAll(numbersToSum ...[]int) []int {
    lengthOfNumbers := len(numbersToSum)
    sums := make([]int, lengthOfNumbers)

    for i, numbers := range numbersToSum {
        sums[i] = Sum(numbers)
    }

    return sums
}
 
slice 생성시 make 를 통해서 생성할 수 있는데 2번째 인자는 capacity 를 나타낸다. 
 
이제 리팩토링을 해보자. make 의 2번째 인자는 capacity를 나타낸다고 했다. 만약 capacity 가 2인 slice 에 sums[10] 와 같이 초과한 index 에 값을 할당하면 error 가 발생한다. Go 는 slice 에 값을 추가한 slice 를 반환하는 append 함수를 제공하는데 이를 이용하면 좀 더 안전하게 slice 에 값을 추가할 수 있다. 
 
func SumAll(numbersToSum ...[]int) []int {
    var sums []int
    for _, numbers := range numbersToSum {
        sums = append(sums, Sum(numbers))
    }

    return sums
}

'Language > Go' 카테고리의 다른 글

Go - pointers & errors  (0) 2021.12.26
Go - struct, method, interface  (0) 2021.12.24
Go - Iteration & Benchmark  (0) 2021.12.04
Go - Go with TDD  (0) 2021.12.04
Go - Go 와 Gin 을 활용한 RESTful API 개발  (0) 2021.11.28

댓글