#0. 테이블 드리븐 테스트에서 사용자 입력에 대한 테스트

사용자가 어떤 값을 입력한다고 가정하여 그 값과 예상되는 값과 일치하는지 비교합니다. 

 

#1. 실습

maim.go와 main_test.go로 테스트 실습.

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func main() {
    // print a welcome message
    intro()
    // create a channel to indicate when the user wants to quit
    doneChan := make(chan bool)
    // start a goroutine to read user input and run program
    go readUserInput(doneChan)
    // block until the doneChan gets a value
    <-doneChan
    // close the channel
    close(doneChan)
    // say goodbye
    fmt.Println("Goodbye.")
}

func readUserInput(doneChan chan bool) {
    scanner := bufio.NewScanner(os.Stdin)
    for {
       res, done := checkNumbers(scanner)
       if done {
          doneChan <- true
          return
       }
       fmt.Println(res)
       prompt()
    }
}

func checkNumbers(scanner *bufio.Scanner) (string, bool) {
    // read user input
    scanner.Scan()
    //check to see if the user wants to quit
    if strings.EqualFold(scanner.Text(), "q") {
       return "", true
    }
    // try to convert what the user typed into an int
    numToCheck, err := strconv.Atoi(scanner.Text())
    if err != nil {
       return "Please enter a whole number", false
    }
    _, msg := isPrime(numToCheck)
    return msg, false
}
func intro() {
    fmt.Println("Is it Prime?")
    fmt.Println("------------")
    fmt.Println("Enter a whole number, and we'll tell you if it is a prime number or not. Enter q to quit.")
    prompt()
}
func prompt() {
    fmt.Print("-> ")
}
func isPrime(n int) (bool, string) {
    // 0과 1 처리(0과 1은 소수가 아님)
    if n == 0 || n == 1 {
       return false, fmt.Sprintf("%d is not prime, by definition!", n)
    }
    // 0보다 작은 수의 경우
    if n < 0 {
       return false, "Negative numbers are not prime, by definition!"
    }
    // 소수인지 아닌지 체크
    for i := 2; i <= n/2; i++ {
       if n%i == 0 {
          // 소수가 아닌 경우
          return false, fmt.Sprintf("%d is not a prime number because it is divisible by %d!", n, i)
       }
    }
    //소수인 경우
    return true, fmt.Sprintf("%d is a prime number!", n)
}

1. main.go 파일

소수인지 체크하는 프로그램으로써. 소수인 경우, 소수가 아닌 경우, 0과1인 경우, 0보다작은 경우(-1)를 체크한다.

그리고 사용자에게 입력을 받고 문자열을 출력하는 함수(intro(), prompt(), checkNumbers(), readUserInput())들을 추가했다.

 

package main

import (
	"bufio"
	"io"
	"os"
	"strings"
	"testing"
)

func Test_isPrime(t *testing.T) {
	primeTests := []struct {
		name     string
		testNum  int    //테스트 할 값
		expected bool   //예상되는 bool값
		msg      string //예상되는 메시지
	}{
		{"prime", 7, true, "7 is a prime number!"},
		{"not prime", 8, false, "8 is not a prime number because it is divisible by 2!"},
		{"zero", 0, false, "0 is not prime, by definition!"},
		{"one", 1, false, "1 is not prime, by definition!"},
		{"negative number", -11, false, "Negative numbers are not prime, by definition!"},
	}

	for _, e := range primeTests {
		result, msg := isPrime(e.testNum)

		//true가 예상되지만 false를 반환한 경우
		if e.expected && !result {
			t.Errorf("%s: expected true but got false", e.name)
		}
		//false가 예상되지만 true를 반환한 경우
		if !e.expected && result {
			t.Errorf("%s: expected false but got true", e.name)
		}

		//예상되는 e.msg값과 반환한 msg값이 다른 경우
		if e.msg != msg {
			t.Errorf("%s: expected %s but got %s", e.name, e.msg, msg)
		}
	}
}

func Test_prompt(t *testing.T) {
	// save a copy of os.Stdout
	oldOut := os.Stdout

	// create a read and write pipe
	r, w, _ := os.Pipe()

	// set os.Stdout to our write pipe
	os.Stdout = w

	prompt()

	// close our write
	_ = w.Close()

	// reset os.Stdout to what it was before
	os.Stdout = oldOut

	// read the output of our prompt() func from our read pipe
	out, _ := io.ReadAll(r)

	// perform our test
	if string(out) != "-> " {
		t.Errorf("incorrect prompt: expected -> but got %s", string(out))
	}
}

func Test_intro(t *testing.T) {
	// save a copy of os.Stdout
	oldOut := os.Stdout

	// create a read and write pipe
	r, w, _ := os.Pipe()

	// set os.Stdout to our write pipe
	os.Stdout = w

	intro()

	// close our write
	_ = w.Close()

	// reset os.Stdout to what it was before
	os.Stdout = oldOut

	// read the output of our prompt() func from our read pipe
	out, _ := io.ReadAll(r)

	if !strings.Contains(string(out), "Enter a whole number") {
		t.Errorf("intro text not correct; got %s", string(out))
	}
}

func Test_checkNumbers(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{name: "empty", input: "", expected: "Please enter a whole number!"},
		{name: "zero", input: "0", expected: "0 is not prime, by definition!"},
		{name: "one", input: "1", expected: "1 is not prime, by definition!"},
		{name: "two", input: "2", expected: "2 is a prime number!"},
		{name: "three", input: "3", expected: "3 is a prime number!"},
		{name: "negative", input: "-1", expected: "Negative numbers are not prime, by definition!"},
		{name: "typed", input: "three", expected: "Please enter a whole number!"},
		{name: "decimal", input: "1.1", expected: "Please enter a whole number!"},
		{name: "quit", input: "q", expected: ""},
		{name: "QUIT", input: "Q", expected: ""},
	}
	for _, e := range tests {
		input := strings.NewReader(e.input)
		reader := bufio.NewScanner(input)
		res, _ := checkNumbers(reader)

		if !strings.EqualFold(res, e.expected) {
			t.Errorf("%s: expected %s, but got %s", e.name, e.expected, res)
		}
	}
}

2. main_test.go 파일 (func Test_checkNumbers)

 1. tests에 테이블 테스트를 위한 구조체 슬라이스 저장.

 2. name(값 이름),  input(사용자가 입력을 했다고 가정), expected(사용자 입력(input)에 따른 예상되는 메세지)

 3. 구조체 슬라이스로 만든 테이블 테스트 슬라이스를 반복문으로 돌면서 확인

 4. input := strings.NewReader(e.input) : e.input에서 문자열을 가져와서 새로운 Reader를 만들어줌.

 5. reader := bufio.NewScanner(input) : input에서 값을 가져와서 새로운 Scanner를 만들어줌.

 6. res, _ := checkNumber(reader) : checkNumbers 함수에 reader를 전달하고 반환 값을 받습니다.

 7. if !strings.EqualFold(res, e.expected) : checkNumber 함수에서 반환한 값과 예상되는 메세지를 비교합니다.

 8. 비교해서 메세지가 똑같으면 성공.

 

#2. 결과

테스트 통과

 

1. go test -v, go test -cover 명령어로 테스트 범위와 통과 확인.

 

html 파일로 테스트 cover 확인

html 파일로 테스트 cover 확인

2. go test . -coverprofile=coverage.out 명령어로 coverage.out 파일을 만들어줌
3. go tool cover -html=coverage.out(윈도우에서는 .out없이)  명령어로 html 파일로 확인. (초록색 부분이 cover(테스트 코드 작성 완료))

 

출처
https://www.udemy.com/course/introduction-to-testing-in-go-golang/

go에있는 빌트인 함수인 append를 공식문서에서 찾아봤습니다.
https://go.dev/doc/effective_go#append

 

Effective Go - The Go Programming Language

Documentation Effective Go Effective Go Introduction Go is a new language. Although it borrows ideas from existing languages, it has unusual properties that make effective Go programs different in character from programs written in its relatives. A straigh

go.dev

 

 

Now we have the missing piece we needed to explain the design of the append built-in function. The signature of append is different from our custom Append function above. Schematically, it's like this:

이제 우리는 append 내장 함수의 디자인을 설명하기 위해서 우리가 필요했던 잃어버린 조각(위에서 부족한 설명에 대한 답)을 가지고 있다. append의 특징은 위에서 본 우리의 커스텀 Append 함수(https://go.dev/doc/effective_go#slices)와 다르다. 개략적으로 이것은 다음과 같다:

 

func append(slice []T, elements ...T) []T

where T is a placeholder for any given type. You can't actually write a function in Go where the type T is determined by the caller. That's why append is built in: it needs support from the compiler.

여기에서 T는 어떤 주어진 타입에 대한 placeholder(제네릭, 일시적으로 값을 대체하거나 나중에 채워질 것으로 예상되는 위치 )이다. Go에서는 타입 T가 호출자에 의해 결정되는 함수를 작성할 수 없다. 이것이 append가 내장함수인 이유이고: 컴파일러의 지원이 필요하다.

 

위의 문장에서 보충설명 : 제네릭 T를 사용할 때 타입을 명시해줘야 된다. 안그러면 오류가 난다. 하지만 위에 append 함수는 호출하는 쪽에서 타입을 정할수 있다. 컴파일러의 지원덕분에 가능. 내장함수 append는 a := append([]int{}, 1, 2, 3)처럼 호출하는 쪽에서 []int로 정할수 있다. 하지만 커스텀 함수에서 제네릭은 이렇게 타입을 명시해줘야함. 여기서는 any로 해줌.

 

func Print[T any](a T) {
   fmt.Println(a)
}

What append does is append the elements to the end of the slice and return the result. The result needs to be returned because, as with our hand-written Append, the underlying array may change. This simple example

append가 하는 것은 요소들을 slice끝에 삽입하고, 결과를 리턴하는 것이다. 우리의 직접 작성된 Append(https://go.dev/doc/effective_go#slices) 와 같이 실제 배열(슬라이스 안에 배열을 가리키는 포인터)은 바뀔수도 있기 때문에, 결과는 반환되어야할 필요가 있다. 이 간단한 예제는

 

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

prints [1 2 3 4 5 6]. So append works a little like Printf, collecting an arbitrary number of arguments.

[1 2 3 4 5 6]을 출력한다. 그래서 append는 약간 Printf처럼 동작하고, 임의의 인자들의 수를 수집한다.

 

But what if we wanted to do what our Append does and append a slice to a slice? Easy: use ... at the call site, just as we did in the call to Output above. This snippet produces identical output to the one above.

그러나 우리의 Append가 하는 것을 하길 원하고, 슬라이스에 슬라이스를 추가하고 싶다면 어떻게 할까? 쉽다: call site(함수 인자에)...을 사용해라, 위에서(https://go.dev/doc/effective_go#Printing) Output(std.Output 함수) 호출에서 했던 것처럼. 아래 snippet(작은 예제)은 위 예제와 동일한 출력을 만든다.

 

위의 문장에서 보충설명 : 위의 custom 함수 Append와 같은 기능을 하고 슬라이스에 슬라이스를 추가하고 싶을 때를 설명하고 있다. 위에 func Println(v ...interface{}) { std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string) } 함수에서 했던 것처럼 ...을 사용 하라는 의미인것으로 보인다.

 

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

Without that ..., it wouldn't compile because the types would be wrong; y is not of type int.

...없이, 위의 예제는 타입들이 틀리기 때문에 컴파일되지 않을 것이다; 예시 : y는 int 타입이 아니다.

https://go.dev/blog/intro-generics

type Ordered interface {
    Integer|Float|~string
}


The expression ~string means the set of all types whose underlying type is string.
~string 표현식은 실제 타입이 string인 모든 타입의 집합을 의미한다.

This includes the type string itself as well as all types declared with definitions such as type MyString string.
이것은 type MyString string와 같은 정의와 함께 선언된 모든 타입들 뿐만 아니라 string타입 자신을 포함합니다.


해석 : type키워드를 사용해서 string을 MyString(Named Type)으로 선언하였다. string과 MyString은 서로 다르다.

 

s1을 string 타입처럼 사용할 수 없다.

 

하지만 ~string을 사용하면 string자신뿐만 아니라 type MyString string로 선언된 타입까지 사용할 수 있다는 뜻이다.

제네릭으로 예제를 만들어보면,

 

제네릭

Integer 인터페이스에 int앞에 tilde(~)를 안 넣어주니 오류가 나온다. MyInt는 Integer를 구현하지 않았다고 나오고 가로 안에 아마 Integer안에 int 앞에 ~를 잃어 버렸다고 한다. Integer 인터페이스 안에 int 앞에 tilde(~) 를 넣어주면 올바르게 작동할 것이다. 이렇게 Integer안에 정의한 자료형들을 underlying type도 허용하고 싶으면 앞에 tilde(~)를 넣어주면 된다.

Close() 메소드를 찾지 못함

go 인 액션 255페이지(ebook) 8장 예제 8.46을 맥북에서 실습중 os.Create()가 *os.File을 f에 반환하는데 f는 Close() 메소드를 찾지 못하고 있어서 이걸로 삽질......

 

https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/os/file_posix.go;l=21 

 

https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/os/file_posix.go;l=21

 

cs.opensource.google

 

Close 메소드 소스를 보니
5번째 줄에 go:build unix || (js && wasm) || windows 로 되어 있음

아마도 내가 맥북을 써서 darwin 운영체제로 기본값이 되어 있을꺼라 예측하고 내 컴퓨터에서 /usr/local/go/src/os/file_posix.go 파일을 찾음

 

file_posix.go 파일이 무시되고 있음.

'file_posix.go' is ignored by the build tool because of the OS mismatch

file_posix.go가 빌드툴에 의해서 무시되고 있다. OS 미스매치 때문에

라고 바로 나옴 Edit settings를 클릭

OS를 변경

go:build unix || (js && wasm) || windows || linux 에 맞춰서 OS를 windows로 변경 후 OK

그러면 Close() 메소드 사용 가능 해짐

 

 

# 맨 아래 코드 먼저 보시고 설명 읽으시길 추천!!!

 

메서드 집합(method sets)

값의 관점에서 본 메서드 집합
메서드 수신자
T (t T)
*T (t *T)

- 설명

T        (t T) : 값 u는 T이고,  notify메서드에서의 수신자 (u user)는 (t T)를 뜻한다.

*T      (t T) 그리고 (t *T) : &u는 *T이고, notify메서드의 수신자 (u *user) 또는 (u user)는 (t T) 와 (t *T)를 뜻한다.

 

수신자의 관점에서 본 메서드 집합
메서드 수신자
(t T) T와 *T
(t *T) *T

-설명

(t T)        T와 *T  : notify메서드에서의 수신자 (u user)는 (t T)이고, u와 &u는 T와 *T를 뜻한다.

(t *T)      *T  : notify메서드의 수신자 (u *user)는 (t *T)이고, &u는 *T를 뜻한다.

 

 

package main

import "fmt"

 

type notifier interface {

    notify()

}

 

type user struct {

    name  string

    email string

}

 

func (u *user) notify() { // (u *user) 수신자

    fmt.Printf("사용자에게 메일을 전송합니다: %s<%s>\n", u.name, u.email)

}

 

func sendNotification(n notifier) {

    n.notify()

}

func main() {

    u := user{"Bill", "bill@email.com"} // u값

    sendNotification(u) //&u로 전달해야 컴파일 가능

}

 

현재 이코드는 작동하지 않는데 이유는 notifier interface가 nofity() 메서드를 가지고 있는데 

notify메서드의 수신자는 u *user(포인터 수신자)이다. 하지만 main에서 sendNotification 인수로 &u가 아닌 u를 전달하고 있다.

위의 메서드 집합에 따르면 메서드 수신자 (u *user) 포인터 이므로 &u 같이 주소만를 전달할 있다. 

 

출처 : 책. Go 인 액션 132쪽 메서드 집합

x := 42 //int
y := 42.34543 // float64

이렇게 golang에서 타입선언 없이 := 연산자와 같이 값을 할당하면 정수는 int로, 실수는 float64로 자동으로 타입이 정해집니다.

int는 컴퓨터 시스템에 따라서 32비트 또는 64비트 입니다.
float64는 실수형 기본값

For instance, int32 and int are not the same type even though they may have the same size on a particular architecture.

int32와 int가 특정 시스템에서 사이즈가 같더라도 같은 타입이 아니라는 말이네요.
쉽게 설명하면 위에서 컴퓨터 시스템에 따라서 int는 32 또는 64라고 했는데 시스템이 32비트면 int는 32비트입니다.
그렇다고 해서 타입 int32와 int가 같은 타입은 아니라는 말입니다.

아키텍쳐 종류 출력

package main

import (
"fmt"
"runtime"
)

func main() {
fmt.Println(runtime.GOARCH) //아키텍쳐 종류
}

출력해보니 amd64가 나오네요.
64비트라는 뜻

+ Recent posts