스모크 테스트 (Smoke Test)

스모크 테스트는 소프트웨어의 가장 중요한 기능이 제대로 작동하는지 빠르게 확인하는 예비 테스트이다.

간단한 웹 애플리케이션의 스모크 테스트 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import requests
import logging

class WebAppSmokeTest:
    def __init__(self, base_url):
        self.base_url = base_url
        self.logger = logging.getLogger(__name__)
    
    def run_smoke_test(self):
        """기본 기능 스모크 테스트 실행"""
        test_results = {
            "homepage_access": self.test_homepage(),
            "login_page": self.test_login_page(),
            "basic_search": self.test_search_functionality(),
            "server_health": self.test_server_status()
        }
        
        # 테스트 결과 분석
        failed_tests = [test for test, result in test_results.items() 
                       if result == False]
        
        if failed_tests:
            self.logger.error(f"스모크 테스트 실패: {failed_tests}")
            return False
        
        self.logger.info("모든 스모크 테스트 통과")
        return True
    
    def test_homepage(self):
        """홈페이지 접속 테스트"""
        try:
            response = requests.get(f"{self.base_url}/")
            return response.status_code == 200
        except Exception as e:
            self.logger.error(f"홈페이지 접속 실패: {str(e)}")
            return False
    
    def test_login_page(self):
        """로그인 페이지 접속 테스트"""
        try:
            response = requests.get(f"{self.base_url}/login")
            return "로그인" in response.text
        except Exception as e:
            self.logger.error(f"로그인 페이지 접속 실패: {str(e)}")
            return False

특징과 목적

스모크 테스트의 주요 특징과 목적은 다음과 같다:

  1. 빠른 검증: 30분에서 1시간 이내에 완료된다.
  2. 핵심 기능 확인: 소프트웨어의 가장 중요한 기능들만 테스트한다.
  3. 안정성 평가: 추가적인 테스트를 진행할 수 있을 만큼 소프트웨어가 안정적인지 판단한다.
  4. 시간과 비용 절약: 심각한 문제를 조기에 발견하여 추가 테스트에 들어가는 시간과 비용을 절약한다.

테스트 범위

스모크 테스트는 다음과 같은 범위를 포함한다:

  1. 핵심 기능: 예를 들어, 온라인 쇼핑몰의 경우 로그인, 상품 검색, 장바구니 추가, 결제 등의 기능을 테스트한다.
  2. 기본적인 사용자 흐름: 사용자가 가장 자주 사용하는 기능들의 기본적인 흐름을 확인한다.

수행 시점

스모크 테스트는 일반적으로 다음과 같은 시점에 수행된다:

  1. 새로운 빌드 배포 직후
  2. 품질 보증(QA) 환경에서 본격적인 테스트를 시작하기 전
  3. 간단한 기능 수정 후 빠르게 운영 환경에 배포해야 할 때[

검증 대상

스모크 테스트의 주요 검증 대상은 다음과 같다:

  1. 핵심 기능의 작동 여부
  2. 기본적인 사용자 인터페이스(UI)
  3. 주요 데이터 흐름
  4. 시스템 안정성

종류

스모크 테스트는 수행 수준에 따라 다음과 같이 분류할 수 있다:

  1. 수락 테스트 수준 스모크 테스트
  2. 시스템 수준 스모크 테스트
  3. 통합 수준 스모크 테스트

진행 방식

스모크 테스트는 다음과 같은 단계로 진행된다:

  1. 중요 기능 식별: 예를 들어, 이메일 서비스의 경우 로그인, 이메일 작성, 전송 기능을 선정한다.
  2. 테스트 케이스 준비: 각 핵심 기능에 대한 간단한 테스트 케이스를 작성한다.
  3. 테스트 실행: 준비된 테스트 케이스를 실행하고 결과를 기록한다.
  4. 결과 분석: 테스트 결과를 분석하여 추가 테스트 진행 여부를 결정한다.

예를 들어, 온라인 쇼핑몰 애플리케이션의 스모크 테스트는 다음과 같이 진행될 수 있다:

  1. 사용자 로그인 확인
  2. 상품 검색 기능 테스트
  3. 장바구니에 상품 추가
  4. 간단한 결제 프로세스 확인
    이러한 핵심 기능들이 정상적으로 작동한다면, 스모크 테스트는 통과한 것으로 간주하고 더 자세한 테스트를 진행할 수 있다.

소프트웨어의 가장 중요하고 핵심적인 기능들이 제대로 작동하는지 빠르게 확인하는 초기 테스트.
이는 마치 새로 지은 건물에 입주하기 전에 기본적인 전기, 수도, 가스가 제대로 작동하는지 확인하는 것과 비슷하다.

유래는 하드웨어 테스트에서 시작되었다. 새로 조립한 하드웨어에 전원을 처음 연결했을 때 연기가 나지 않으면 기본적인 테스트는 통과한 것으로 보는 관행에서 유래했다.

주요 특징:

  1. 신속성: 빠르게 실행되어 즉각적인 피드백을 제공합니다.
  2. 표면적 검사: 깊이 있는 테스트가 아닌 기본적인 기능 확인에 집중합니다.
  3. 치명적 결함 발견: 시스템의 가장 심각한 문제들을 초기에 발견할 수 있습니다.
  4. CI/CD 통합: 지속적 통합 과정에서 첫 번째 관문으로 활용됩니다.

예시

  • 테스트 범위의 선택
    • 가장 중요한 기능들만 선별하여 테스트합니다.
    • 깊이 있는 테스트 대신 기본적인 작동 여부만 확인합니다.
  • 빠른 실행
    • 각 테스트는 최소한의 검증만 수행합니다.
    • 불필요한 대기 시간을 최소화합니다.
  • 명확한 결과 보고
    • 각 테스트의 성공/실패 여부를 명확히 기록합니다.
    • 실패 시 문제를 빠르게 파악할 수 있도록 상세 정보를 제공합니다.
  • 자동화 고려
    • CI/CD 파이프라인에 쉽게 통합될 수 있도록 설계되었습니다.
    • 명확한 종료 상태를 제공합니다.

Python

웹 애플리케이션의 스모크 테스트

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import requests
import pytest
import logging
from datetime import datetime

class WebAppSmokeTest:
    """웹 애플리케이션의 핵심 기능을 검증하는 스모크 테스트"""
    
    def __init__(self, base_url):
        self.base_url = base_url
        self.session = requests.Session()
        self.setup_logging()
    
    def setup_logging(self):
        """테스트 결과 로깅 설정"""
        logging.basicConfig(
            filename=f'smoke_test_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log',
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s'
        )
    
    def log_test_result(self, test_name, success, message):
        """테스트 결과를 로그에 기록"""
        status = "PASS" if success else "FAIL"
        logging.info(f"{test_name}: {status} - {message}")

    def test_homepage_accessibility(self):
        """홈페이지 접근 가능성 테스트"""
        try:
            response = self.session.get(f"{self.base_url}/")
            success = response.status_code == 200
            self.log_test_result(
                "Homepage Access", 
                success,
                f"Status Code: {response.status_code}"
            )
            return success
        except Exception as e:
            self.log_test_result("Homepage Access", False, str(e))
            return False

    def test_login_functionality(self, username, password):
        """로그인 기능 테스트"""
        try:
            response = self.session.post(
                f"{self.base_url}/login",
                json={"username": username, "password": password}
            )
            success = response.status_code == 200 and "token" in response.json()
            self.log_test_result(
                "Login Function", 
                success,
                f"Status Code: {response.status_code}"
            )
            return success
        except Exception as e:
            self.log_test_result("Login Function", False, str(e))
            return False

    def test_basic_database(self):
        """데이터베이스 연결 테스트"""
        try:
            response = self.session.get(f"{self.base_url}/health/db")
            success = response.status_code == 200
            self.log_test_result(
                "Database Connection", 
                success,
                f"Status Code: {response.status_code}"
            )
            return success
        except Exception as e:
            self.log_test_result("Database Connection", False, str(e))
            return False

    def test_api_endpoints(self):
        """주요 API 엔드포인트 테스트"""
        endpoints = ['/api/users', '/api/products', '/api/orders']
        results = []
        
        for endpoint in endpoints:
            try:
                response = self.session.get(f"{self.base_url}{endpoint}")
                success = response.status_code in [200, 201]
                self.log_test_result(
                    f"API Endpoint {endpoint}", 
                    success,
                    f"Status Code: {response.status_code}"
                )
                results.append(success)
            except Exception as e:
                self.log_test_result(f"API Endpoint {endpoint}", False, str(e))
                results.append(False)
        
        return all(results)

    def run_all_tests(self):
        """모든 스모크 테스트 실행"""
        logging.info("Starting Smoke Tests...")
        
        test_results = {
            "homepage": self.test_homepage_accessibility(),
            "login": self.test_login_functionality("test_user", "test_pass"),
            "database": self.test_basic_database(),
            "api": self.test_api_endpoints()
        }
        
        all_passed = all(test_results.values())
        summary = "All tests passed!" if all_passed else "Some tests failed!"
        logging.info(f"Smoke Test Summary: {summary}")
        
        return test_results

# 테스트 실행 예시
if __name__ == "__main__":
    smoke_test = WebAppSmokeTest("http://example.com")
    results = smoke_test.run_all_tests()
    print("Smoke Test Results:", results)

Javascript

프론트엔드 애플리케이션의 스모크 테스트

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
const puppeteer = require('puppeteer');

class FrontendSmokeTest {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.browser = null;
        this.page = null;
        this.testResults = [];
    }

    async initialize() {
        try {
            this.browser = await puppeteer.launch({
                headless: true,
                args: ['--no-sandbox']
            });
            this.page = await this.browser.newPage();
            
            // 페이지 로드 타임아웃 설정
            this.page.setDefaultTimeout(10000);
            
            // 콘솔 로그 캡처
            this.page.on('console', msg => {
                console.log(`Browser Console: ${msg.text()}`);
            });
        } catch (error) {
            console.error('Browser initialization failed:', error);
            throw error;
        }
    }

    async logTestResult(testName, success, message) {
        const result = {
            testName,
            success,
            message,
            timestamp: new Date().toISOString()
        };
        
        this.testResults.push(result);
        console.log(`${testName}: ${success ? 'PASS' : 'FAIL'} - ${message}`);
    }

    async testPageLoad() {
        try {
            const startTime = Date.now();
            await this.page.goto(this.baseUrl);
            const loadTime = Date.now() - startTime;
            
            const title = await this.page.title();
            await this.logTestResult(
                'Page Load',
                true,
                `Page loaded in ${loadTime}ms, Title: ${title}`
            );
            return true;
        } catch (error) {
            await this.logTestResult('Page Load', false, error.message);
            return false;
        }
    }

    async testNavigation() {
        try {
            const navLinks = await this.page.$$('nav a');
            let success = true;
            
            for (const link of navLinks) {
                const href = await link.evaluate(el => el.href);
                const text = await link.evaluate(el => el.textContent);
                
                try {
                    await Promise.all([
                        this.page.waitForNavigation(),
                        link.click()
                    ]);
                    
                    await this.logTestResult(
                        `Navigation - ${text}`,
                        true,
                        `Successfully navigated to ${href}`
                    );
                } catch (error) {
                    success = false;
                    await this.logTestResult(
                        `Navigation - ${text}`,
                        false,
                        error.message
                    );
                }
            }
            
            return success;
        } catch (error) {
            await this.logTestResult('Navigation Test', false, error.message);
            return false;
        }
    }

    async testFormSubmission() {
        try {
            // 로그인 폼 테스트
            await this.page.goto(`${this.baseUrl}/login`);
            
            await this.page.type('input[name="username"]', 'test_user');
            await this.page.type('input[name="password"]', 'test_password');
            
            await Promise.all([
                this.page.waitForNavigation(),
                this.page.click('button[type="submit"]')
            ]);
            
            const success = await this.page.evaluate(() => {
                return !document.querySelector('.error-message');
            });
            
            await this.logTestResult(
                'Form Submission',
                success,
                success ? 'Login form submitted successfully' : 'Login form submission failed'
            );
            
            return success;
        } catch (error) {
            await this.logTestResult('Form Submission', false, error.message);
            return false;
        }
    }

    async testAPIIntegration() {
        try {
            // API 엔드포인트 호출 테스트
            const response = await this.page.evaluate(async () => {
                const res = await fetch('/api/health');
                return res.ok;
            });
            
            await this.logTestResult(
                'API Integration',
                response,
                response ? 'API health check passed' : 'API health check failed'
            );
            
            return response;
        } catch (error) {
            await this.logTestResult('API Integration', false, error.message);
            return false;
        }
    }

    async runAllTests() {
        console.log('Starting Frontend Smoke Tests...');
        
        try {
            await this.initialize();
            
            const results = {
                pageLoad: await this.testPageLoad(),
                navigation: await this.testNavigation(),
                formSubmission: await this.testFormSubmission(),
                apiIntegration: await this.testAPIIntegration()
            };
            
            const allPassed = Object.values(results).every(result => result);
            console.log('\nSmoke Test Summary:');
            console.log(results);
            console.log(`Overall Status: ${allPassed ? 'PASS' : 'FAIL'}`);
            
            return results;
        } catch (error) {
            console.error('Smoke test failed:', error);
            throw error;
        } finally {
            if (this.browser) {
                await this.browser.close();
            }
        }
    }

    async generateReport() {
        const successCount = this.testResults.filter(r => r.success).length;
        const failCount = this.testResults.length - successCount;
        
        const report = {
            timestamp: new Date().toISOString(),
            totalTests: this.testResults.length,
            successCount,
            failCount,
            successRate: `${((successCount / this.testResults.length) * 100).toFixed(2)}%`,
            results: this.testResults
        };
        
        console.log('\nTest Report:', JSON.stringify(report, null, 2));
        return report;
    }
}

// 사용 예시
async function runSmokeTests() {
    const smokeTest = new FrontendSmokeTest('http://example.com');
    try {
        await smokeTest.runAllTests();
        await smokeTest.generateReport();
    } catch (error) {
        console.error('Smoke test execution failed:', error);
    }
}

runSmokeTests();

참고 및 출처