๐ŸŽฏ Selenium์„ ํ†ตํ•ด E2E ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“— Today I Learned

Selenium

Selenium์€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ž๋™์œผ๋กœ ์ œ์–ดํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ๋ธŒ๋ผ์šฐ์ €์™€ ์–ธ์–ด๋ฅผ ์ง€์›ํ•˜๋ฉฐ, ์‹ค์ œ ์‚ฌ์šฉ์ž์˜ ํ–‰๋™์„ ํ‰๋‚ด ๋‚ด๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.


์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ

  • Selenium WebDriver

    • ๋‹ค์–‘ํ•œ ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™”๋ฅผ ์ง€์›ํ•˜๋Š” ๋“œ๋ผ์ด๋ฒ„ (Chrome, Firefox ๋“ฑ)
  • Selenium IDE

    • ๋ธŒ๋ผ์šฐ์ € ์ƒ์˜ ์‚ฌ์šฉ์ž ๋™์ž‘์„ ๋…นํ™”/์žฌ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ (Chrome/Firefox ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ)
  • Selenium Grid

    • ์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ ๋จธ์‹ ์— ๋ณ‘๋ ฌ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๋ถ„์‚ฐํ•ด ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ถ„์‚ฐ ์‹คํ–‰ ํ”Œ๋žซํผ
      ์ถœ์ฒ˜: selenium



Selenium + Docker ๊ตฌ์„ฑ

  • ์‚ฌ์šฉ ์ด๋ฏธ์ง€: selenium/standalone-chrome ( Apple Silicon(M1/M2 ๋“ฑ)์—์„œ๋Š” seleniarm/standalone-chromium ์‚ฌ์šฉ )

  • ํฌํŠธ: 4444 โ†’ Selenium Web UI + WebDriver ์ ‘๊ทผ ํฌํŠธ

  • ์‹คํ–‰ ๋ช…๋ น์–ด

docker run -d --rm -p 4444:4444 \
  -v /dev/shm:/dev/shm \
  selenium/standalone-chrome
  • ํ™•์ธ ์ฃผ์†Œ: http://localhost:4444

โ— ์‹ค์Šต์—์„œ๋Š” Grid(hub/node ๋ถ„๋ฆฌ)๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , standalone ์ด๋ฏธ์ง€๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.




Python ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํ๋ฆ„


  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์˜ˆ์‹œ (first_test.py)
from selenium import webdriver
import time

options = webdriver.ChromeOptions()
options.add_argument('--ignore-ssl-errors=yes')
options.add_argument('--ignore-certificate-errors')

driver = webdriver.Remote(
  command_executor='http://localhost:4444/wd/hub',
  options=options
)

driver.maximize_window()
time.sleep(10)  # ํ™•์ธ์šฉ ์ง€์—ฐ

driver.get("https://notes.prgms-fullcycle.com")  # ํŽ˜์ด์ง€ ์—ด๊ธฐ
time.sleep(10)

driver.find_element("link text", "๋ฌด๋ฃŒ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ").click()  # ๋ฒ„ํŠผ ํด๋ฆญ
time.sleep(10)

s = input("Done: ")  # ์‚ฌ์šฉ์ž๊ฐ€ Enter ๋ˆ„๋ฅผ ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
driver.close()
driver.quit()

print("Test Execution Successfully Completed!")
  • ํ•œ๊ณ„: ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ์‹œ์—ฐ์ผ ๋ฟ, ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๊ฒ€์ฆ(assertion)์ด ์—†์Œ

    • ํŠน์ • ์š”์†Œ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ (assert)

    • ํŽ˜์ด์ง€ ์ด๋™ ๊ฒฐ๊ณผ ๊ฒ€์ฆ

    • ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์ฒ˜๋ฆฌ ๋“ฑ ํ…Œ์ŠคํŠธ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ ํ•„์š”




E2E ํ…Œ์ŠคํŠธ

์‚ฌ์šฉ์ž์˜ ๊ด€์ ์—์„œ ์‹œ์Šคํ…œ ์ „์ฒด๋ฅผ ์ž…๋ ฅ โ†’ ์ถœ๋ ฅ๊นŒ์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ ๋ฐฉ์‹

E2E ํ…Œ์ŠคํŠธ์˜ ํŠน์ง•

  • ์‹œ์Šคํ…œ ๋‚ด๋ถ€ ๋กœ์ง์€ ๋ณด์ง€ ์•Š์Œ (Black-box ํ…Œ์ŠคํŠธ)

  • ์‹ค์ œ ์‚ฌ์šฉ์ž์ฒ˜๋Ÿผ ์ž…๋ ฅํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•จ

  • ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ๋Š” ํ™•์ธํ•  ์ˆ˜ ์—†๋Š” ํ๋ฆ„์„ ๊ฒ€์ฆ ๊ฐ€๋Šฅ


ํ”„๋กœ์ ํŠธ ์ ์šฉ

  1. Selenium standalone (Chrome)์„ ๋กœ์ปฌ Kubernetes ํด๋Ÿฌ์Šคํ„ฐ์— ์„ค์น˜

  2. Selenium IDE๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋…นํ™” โ†’ ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜

  3. ๋ณ€ํ™˜๋œ ์ฝ”๋“œ๋ฅผ ๋ณด์™„ํ•˜์—ฌ ์ž๋™ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ ํ™•๋ณด

  4. ํ–ฅํ›„ CI/CD ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ




Selenium IDE๋กœ ์‹œ๋‚˜๋ฆฌ์˜ค ๋…นํ™”

1. Firefox ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์„ค์น˜

โš ๏ธ 2024๋…„ ๊ธฐ์ค€, Chrome ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์ •์ฑ…(Manifest V3) ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•ด Selenium IDE๋Š” ๋” ์ด์ƒ Chrome์—์„œ ์„ค์น˜ํ•  ์ˆ˜ ์—†๊ณ , ๊ณต์‹์ ์œผ๋กœ๋„ ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • Firefox์—์„œ Selenium IDE์„ ์„ค์น˜

2. ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ๋…นํ™”

  • REC ๋ฒ„ํŠผ ํด๋ฆญ โ†’ Base URL ์ž…๋ ฅ

  • ์ž๋™์œผ๋กœ ์—ด๋ฆฌ๋Š” ํŽ˜์ด์ง€์—์„œ ์‹œ๋‚˜๋ฆฌ์˜ค ์ˆ˜ํ–‰

    • "๋ฌด๋ฃŒ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ" ํด๋ฆญ

    • ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ

    • ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ

    • ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ ํด๋ฆญ


3. ์‹œ๋‚˜๋ฆฌ์˜ค ์ €์žฅ

.side ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง„ ํŒŒ์ผ(simple_test.side)๋กœ ์ €์žฅ๋จ. ์ด ํŒŒ์ผ์€ ๋‚˜์ค‘์— ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์™€ ์žฌ์ƒํ•˜๊ฑฐ๋‚˜ ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ


4. ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ฝ”๋“œ ์ƒ์„ฑ

Untitled๋กœ ์ƒ์„ฑ๋œ ํŒŒ์ผ์„ ์šฐํด๋ฆญํ•˜์—ฌ Export๋ฅผ ๋ˆ„๋ฅด๊ณ , Python pytest๋ฅผ ๋ˆŒ๋Ÿฌ Export ํ•ฉ๋‹ˆ๋‹ค.

from selenium import webdriver
from selenium.webdriver.common.by import By

class TestUntitled():
    def setup_method(self, method):
        self.driver = webdriver.Chrome()

    def teardown_method(self, method):
        self.driver.quit()

    def test_untitled(self):
        self.driver.get("http://localhost:30030/")
        self.driver.find_element(By.LINK_TEXT, "๋ฌด๋ฃŒ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ").click()
        self.driver.find_element(By.NAME, "email").send_keys("test@example.com")
        self.driver.find_element(By.NAME, "password").send_keys("1234")
        self.driver.find_element(By.ID, "login-button").click()
        self.driver.implicitly_wait(2)  # ๋กœ๋”ฉ ๋Œ€๊ธฐ
        self.driver.find_element(By.CSS_SELECTOR, "#logout-button > span").click()

๐Ÿ’ก E2E ํ…Œ์ŠคํŠธ์—์„œ๋Š” ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์‹œ๊ฐ„์ด๋‚˜ ์š”์†Œ ๋ Œ๋”๋ง ์ง€์—ฐ์œผ๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Selenium์˜ wait ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋Œ€๊ธฐ ์ „๋žต

  • Implicit Wait: ์ •ํ•ด์ง„ ์‹œ๊ฐ„ ๋™์•ˆ ์š”์†Œ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
  • Explicit Wait: ํŠน์ • ์กฐ๊ฑด์ด ์ถฉ์กฑ๋  ๋•Œ๊นŒ์ง€ ํด๋ง(polling)ํ•˜๋ฉด์„œ ๊ธฐ๋‹ค๋ฆผ

5. ํ…Œ์ŠคํŠธ ์‹คํ–‰ (pytest ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•จ)

pytest test_untitled.py



ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ตฌ์„ฑ

๊ณตํ†ต ๋กœ์ง: ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ๋ฉ”์„œ๋“œํ™”

def login(self):
    self.driver.find_element(By.LINK_TEXT, "๋ฌด๋ฃŒ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ").click()
    self.driver.find_element(By.NAME, "email").send_keys("test@example.com")
    self.driver.find_element(By.NAME, "password").send_keys("1234")
    self.driver.find_element(By.ID, "login-button").click()

def logout(self):
    self.driver.find_element(By.CSS_SELECTOR, "#logout-button > span").click()

ํ…Œ์ŠคํŠธ ์‹œ์ž‘๊ณผ ์ข…๋ฃŒ ์‹œ์—๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

 def setup_method(self, method):
    self.driver = webdriver.Chrome()
    self.login()

def teardown_method(self, method):
    self.logout()
    self.driver.quit()

๋…ธํŠธ ๋ชฉ๋ก ๋ฐ ๋…ธํŠธ ์ƒ์„ธ ํ…Œ์ŠคํŠธ

def test_noteview(self):
    self.driver.get(BASE_URL + "/notes")
    self.driver.implicitly_wait(10)

    # ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด ํ™•์ธ
    assert self.driver.find_element(By.ID, "current-user").text == "test@example.com"

    # ๋…ธํŠธ ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
    notes_list = self.driver.find_element(By.ID, "notes-list")
    assert notes_list.find_element(By.CSS_SELECTOR, "li:nth-child(1) span").text == "Test (2)"
    assert notes_list.find_element(By.CSS_SELECTOR, "li:nth-child(2) span").text == "Test (1)"

    # ๋…ธํŠธ ์„ ํƒ ํ›„ ๋‚ด์šฉ ๊ฒ€์ฆ
    notes_list.find_element(By.XPATH, "li[last()]/a/span").click()
    self.driver.implicitly_wait(2)
    assert self.driver.find_element(By.CSS_SELECTOR, "article header textarea").text == "Test (1)"
    assert self.driver.find_element(By.CSS_SELECTOR, "article main div div").get_attribute("innerHTML") == \
        "<p>This note is for testing.</p><p>Note number: 1</p>"

๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค์™€ Alert ์ฒ˜๋ฆฌ

def test_loginfail(self):
    self.driver.get("http://localhost:30030/")
    self.driver.find_element(By.LINK_TEXT, "๋ฌด๋ฃŒ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ").click()
    self.driver.find_element(By.NAME, "email").send_keys("test@example.com")
    self.driver.find_element(By.NAME, "password").send_keys("1235")  # ์ž˜๋ชป๋œ ๋น„๋ฒˆ
    self.driver.find_element(By.ID, "login-button").click()

    alert = self.driver.switch_to.alert
    assert alert.text == "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
    alert.accept()



โœ๏ธ ํšŒ๊ณ 

์‚ฌ์šฉ์ž ๊ด€์ ์˜ ํ๋ฆ„์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด ์ข€ ๋†€๋ผ์šด ๊ฑฐ ๊ฐ™๋‹ค.

profile
๐ŸŒฑ๊ฐœ๋ฐœ ๊ธฐ๋ก์žฅ

0๊ฐœ์˜ ๋Œ“๊ธ€