ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Go 이미지 생성: 완벽한 테스트 전략으로 버그 없는 코드 작성하기
    Go 2024. 9. 20. 17:32

    Go 이미지 생성: 완벽한 테스트 전략으로 버그 없는 코드 작성하기

    Go 언어는 성능과 효율성으로 인정받는 언어지만, 이미지 생성과 같은 복잡한 작업에서는 예상치 못한 버그에 직면할 가능성이 높습니다.

     

    라이브러리 업데이트나 작은 코드 변경에도 이미지 생성 결과가 달라질 수 있기 때문에, 철저한 테스트 전략은 필수적입니다.

     

    이 글에서는 Go를 이용한 이미지 생성 과정에서 효과적인 테스트를 구현하는 방법을 다양한 예시와 함께 자세히 설명합니다.

     

    단순히 테스트 코드 작성법을 넘어, 안정적이고 유지보수가 용이한 코드를 작성하는 데 필요한 전반적인 전략을 제시합니다.

    이미지 생성 함수 예시

    먼저, 테스트 대상이 될 간단한 이미지 생성 함수를 살펴보겠습니다.

     

    이 함수는 입력 문자열을 이미지에 출력하고 JPEG 형식으로 저장하는 기능을 합니다.

     

    실제 라이브러리 호출 부분은 간략화하여 핵심 로직에 집중하도록 했습니다.

    package main
    
    import (
            "bytes"
            "errors"
            "image"
            "image/jpeg"
            "io"
    )
    
    
    // 이미지 생성 함수.  실제 구현은 생략하고 테스트에 필요한 부분만 포함.
    func generateImage(text string, quality int) (io.Reader, error) {
            if text == "" {
                    return nil, errors.New("text cannot be empty")
            }
    
            // 이미지 생성 로직 (실제 구현은 생략)
            // ...  이미지 생성 및 폰트 설정 등의 복잡한 로직이 여기에 들어감 ...
            img := createImage(text) // 실제 구현은 생략
    
            buf := new(bytes.Buffer)
            err := jpeg.Encode(buf, img, &jpeg.Options{Quality: quality})
            if err != nil {
                    return nil, err
            }
            return buf, nil
    }
    
    //  실제 이미지 생성 함수 (구현 생략)
    func createImage(text string) image.Image {
            // 실제 이미지 생성 로직 (여기에 이미지 생성 라이브러리 사용)
            return nil // 실제 구현 생략
    }

     

    이 함수는 text (출력 문자열)과 quality (JPEG 품질)을 입력받아 JPEG 이미지 데이터를 io.Reader로 반환합니다.

     

    실제 이미지 생성 로직(createImage 함수)은 복잡하므로 생략하고, 테스트에 필요한 부분만 구현했습니다.

     

    효과적인 테스트 전략

    이미지 생성 함수 테스트는 단순히 결과 이미지를 확인하는 것만으로는 부족합니다.

     

    다음과 같은 전략을 통해 보다 효과적인 테스트를 수행할 수 있습니다.

     

    1. 단위 테스트 (Unit Test):

    generateImage 함수의 각 부분을 독립적으로 테스트합니다.

     

    입력 값에 따른 에러 처리, createImage 함수의 결과값 검증 등을 포함합니다. createImage 함수 자체는 모킹(mocking)을 통해 테스트할 수 있습니다.

    package main
    
    import (
            "bytes"
            "image"
            "testing"
    )
    
    
    func TestGenerateImage_EmptyText(t *testing.T) {
            _, err := generateImage("", 100)
            if err == nil {
                    t.Error("Expected error for empty text, but got nil")
            }
    }
    
    
    func TestGenerateImage_ValidText(t *testing.T) {
            mockImage := &mockImage{} // 모킹을 위한 mockImage 구조체
            createImage = func(text string) image.Image { return mockImage } // createImage 함수 모킹
    
            reader, err := generateImage("Test Image", 90)
            if err != nil {
                    t.Errorf("Unexpected error: %v", err)
            }
    
            // reader의 내용 검증 (JPEG 데이터 확인)
            // ... (JPEG 데이터 검증 로직) ...
            buf := new(bytes.Buffer)
            _, err = io.Copy(buf, reader)
            if err != nil{
                    t.Errorf("Unexpected error: %v", err)
            }
        //  여기에 buf의 내용을 검증하는 로직 추가 (예: 파일과 비교, 크기 검증 등)
    }
    
    // mockImage 구조체 (createImage 함수 모킹을 위한 구조체)
    type mockImage struct{}
    
    func (m *mockImage) ColorModel() color.Model { return nil }
    func (m *mockImage) Bounds() image.Rectangle { return image.Rect(0, 0, 0, 0) }
    func (m *mockImage) At(x, y int) color.Color { return nil }

     

    2. 통합 테스트 (Integration Test):

     

    generateImage 함수를 실제 이미지 생성 라이브러리와 함께 테스트합니다.

     

    실제 이미지 파일을 생성하고, 파일 크기, 이미지 형식, 그리고 quality 값에 따른 이미지 품질 변화 등을 검증합니다.

     

    임시 디렉토리를 사용하여 테스트 후 생성된 파일을 자동으로 삭제하는 것이 좋습니다.

    package main
    
    import (
            "io/ioutil"
            "os"
            "path/filepath"
            "testing"
    )
    
    func TestGenerateImage_Integration(t *testing.T) {
            tempDir, err := ioutil.TempDir("", "image-test")
            if err != nil {
                    t.Fatal(err)
            }
            defer os.RemoveAll(tempDir)
    
            imagePath := filepath.Join(tempDir, "test.jpg")
            reader, err := generateImage("Integration Test", 80)
            if err != nil {
                    t.Fatalf("Unexpected error: %v", err)
            }
    
            // 이미지 파일 생성 및 검증
            file, err := os.Create(imagePath)
            if err != nil {
                    t.Fatal(err)
            }
            defer file.Close()
    
            _, err = io.Copy(file, reader)
            if err != nil {
                    t.Fatal(err)
            }
    
    
            fileInfo, err := os.Stat(imagePath)
            if err != nil {
                    t.Fatal(err)
            }
            // 파일 크기 검증 등 추가적인 검증 로직 추가
    }

     

    3. 비교 테스트 (Comparison Test):

     

    기준 이미지 파일과 생성된 이미지 파일을 비교합니다. 이미지 비교 라이브러리를 사용하여 이미지의 차이를 정량적으로 측정하고 허용 오차 범위 내에 있는지 확인합니다. (이미지 비교 라이브러리 사용 예시는 생략)

     

    4. 성능 테스트 (Performance Test):

     

    이미지 생성 속도를 측정하고, 성능 저하를 방지합니다.

     

    특히 이미지 크기가 클 경우 성능 테스트는 매우 중요합니다. testing 패키지의 Benchmark 함수를 사용하여 벤치마크 테스트를 수행할 수 있습니다.

     

    결론

    Go 언어를 사용한 이미지 생성 작업에서 안정성을 확보하기 위해서는 체계적인 테스트 전략이 필수적입니다.

     

    단위 테스트, 통합 테스트, 비교 테스트, 성능 테스트를 적절히 조합하여 사용하면 버그를 조기에 발견하고, 코드의 품질과 유지보수성을 높일 수 있습니다.

     

    이 글에서 제시된 예시 코드와 전략을 바탕으로 여러분의 Go 프로젝트에 적합한 테스트 환경을 구축하시길 바랍니다.

     

    특히, 이미지 비교 라이브러리와 모킹 라이브러리를 적절히 활용하면 더욱 효과적인 테스트를 수행할 수 있습니다.

     


    전체코드입니다.

    package main
    
    import (
    	"errors"
    	"flag"
    	"fmt"
    	"image"
    	"image/draw"
    	"image/jpeg"
    	"io"
    	"io/ioutil"
    	"os"
    
    	"github.com/golang/freetype/truetype"
    	"golang.org/x/image/font"
    	"golang.org/x/image/math/fixed"
    )
    
    func main() {
    	var drawValue, outputFilepath string
    	flag.StringVar(&drawValue, "d", "", "Draw value.")
    	flag.StringVar(&outputFilepath, "o", "", "Output filepath.")
    	flag.Parse()
    
    	f, err := os.Create(outputFilepath)
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "%v.", err)
    		os.Exit(1)
    	}
    
    	if err := run(drawValue, f); err != nil {
    		fmt.Fprintf(os.Stderr, "%v.", err)
    		os.Exit(2)
    	}
    }
    
    func run(drawValue string, w io.Writer) error {
    	if drawValue == "" {
    		return errors.New("draw value is empty")
    	}
    
    	baseImg, err := getBaseImage()
    	if err != nil {
    		return err
    	}
    
    	fo, err := getFont()
    	if err != nil {
    		return err
    	}
    
    	drawImg := drawStringToImage(baseImg, fo, drawValue)
    	return jpeg.Encode(w, drawImg, &jpeg.Options{Quality: 100})
    }
    
    func getBaseImage() (image.Image, error) {
    	f, err := os.Open("static_files/images/src.jpg")
    	if err != nil {
    		return nil, err
    	}
    	defer f.Close()
    
    	img, _, err := image.Decode(f)
    	if err != nil {
    		return nil, err
    	}
    
    	return img, nil
    }
    
    func getFont() (*truetype.Font, error) {
    	bs, err := ioutil.ReadFile("static_files/fonts/roboto-regular.ttf")
    	if err != nil {
    		return nil, err
    	}
    
    	fo, err := truetype.Parse(bs)
    	if err != nil {
    		return nil, err
    	}
    
    	return fo, nil
    }
    
    func drawStringToImage(baseImg image.Image, drawFont *truetype.Font, drawValue string) image.Image {
    	r := baseImg.Bounds()
    	drawImg := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
    	draw.Draw(drawImg, drawImg.Bounds(), baseImg, r.Min, draw.Src)
    
    	d := &font.Drawer{Dst: drawImg, Src: image.Black}
    	d.Face = truetype.NewFace(drawFont, &truetype.Options{Size: 20, DPI: 350})
    	d.Dot = fixed.Point26_6{
    		X: (fixed.I(r.Dx()) - d.MeasureString(drawValue)) / 2,
    		Y: fixed.I(r.Dy() / 2),
    	}
    
    	d.DrawString(drawValue)
    	return d.Dst
    }
Designed by Tistory.