Testing Graphical User Interfaces - The Fuzzing Book
Testing Graphical User Interfaces - The Fuzzing Book
In this chapter, we explore how to generate tests for Graphical User Interfaces (GUIs), abstracting from our previous examples on Web testing. Building on general means to extract user interface elements and activate them, our techniques generalize to arbi
www.fuzzingbook.org
0. Lessons
- Selenium is a powerful framework for interacting with user interfaces, especially Web-based user interfaces.
- A finite state model can encode user interface states and transitions.
- Encoding user interface models into a grammar integrates generating text (for forms) and generating user interactions (for navigating)
- To systematically explore a user interface, cover all state transitions, which is equivalent to covering all expansion alternatives in the equivalent grammar.
1. Automated GUI Interaction
UI test framework
: user interface testing에 이용
: 사용가능한 user interface요소에 대해서 test중인 프로그램을 query
: UI요소와 상호작용할 수 있는 방법을 query
Selenium
: Web application을 testing하는 강력한 framework
: (이전) webbrowser()를 통해 홈페이지의 HTML을 검색하고 사용
: 웹브라우저 launch, user interface의 state를 query하고 interact 가능한 API들을 제공
2. Remote Control with Selenium
def start_webdriver(browser=BROWSER, headless=HEADLESS, zoom=1.4):
if browser == 'firefox':
options = webdriver.FirefoxOptions()
if browser == 'chrome':
options = webdriver.ChromeOptions()
if headless and browser == 'chrome':
options.add_argument('headless')
else:
options.headless = headless
# Start the browser, and obtain a _web driver_ object such that we can interact with it.
if browser == 'firefox':
# For firefox, set a higher resolution for our screenshots
options.set_preference("layout.css.devPixelsPerPx", repr(zoom))
gui_driver = webdriver.Firefox(options=options)
# We set the window size such that it fits our order form exactly;
# this is useful for not wasting too much space when taking screen shots.
gui_driver.set_window_size(700, 300)
elif browser == 'chrome':
gui_driver = webdriver.Chrome(options=options)
gui_driver.set_window_size(700, 210 if headless else 340)
return gui_driver
gui_driver = start_webdriver(browser=BROWSER, headless=HEADLESS)
위의 함수는 지정한 option에 따라 web driver를 시작하며
반환된 web driver를 이용하여 web browser를 제어하고 상호작용가능하다
# 주어진 url로 웹드라이버를 사용하여 웹페이지를 이동
gui_driver.get(httpd_url)
# 결과 도출
print_httpd_messages()
127.0.0.1 - - [07/Jan/2023 15:53:23] "GET / HTTP/1.1" 200 -
3. Filling out Forms
Selenium과 broswer를 통해 web page에서 상호작용하기 위해
각각의 요소에 대해서 querySelenium을 이용할 수 있다.
name 요소에 access해보자
from selenium.webdriver.common.by import By
name = gui_driver.find_element(By.NAME, "name")
name.send_keys("Jane Doe")
Image(gui_driver.get_screenshot_as_png())

체크박스의 경우 다음과 같다.
terms = gui_driver.find_element(By.NAME, 'terms')
terms.click() #click을 통해 access
위의 과정과 동일하게 모든 요소(name, email, city, zip code)에 대해서 다 채웠다고 가정하고
submit버튼을 눌러보자
submit = gui_driver.find_element(By.NAME, 'submit')
submit.click()
다음과 같이 모든 요소에 대해 access한 것을 볼 수 있다.
print_httpd_messages()
127.0.0.1 - - [07/Jan/2023 15:53:23] INSERT INTO orders VALUES ('tshirt', 'Jane Doe', 'j.doe@example.com', 'Seattle', '98104')
127.0.0.1 - - [07/Jan/2023 15:53:23] "GET /order?item=tshirt&name=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=98104&terms=on&submit=Place+order HTTP/1.1" 200 -
4. Navigating
link를 클릭하여서 navigate해보자
# <a>태그인 모든 요소를 찾아 찾은 모든 링크 요소를 저장하는 변수
links = gui_driver.find_elements(By.TAG_NAME, "a")
links[0].get_attribute('href')
# link의 0번째 요소의 href속성값을 가져오기
'http://127.0.0.1:8800/terms'
클릭시
links[0].click()
print_httpd_messages()
#결과
# 127.0.0.1 - - [07/Jan/2023 15:53:23] "GET /terms HTTP/1.1" 200 -
5. Writing Test Cases
code snippets를 통해서 예상과 동일하게 interaction이 잘 작동되는 지 확인할 수 있다.
title 요소를 검색하고 이것이 thank you라는 메세지를 포함하고있는 지 확인해보자
def test_successful_order(driver, url):
#정보저장
name = "Walter White"
email = "white@jpwynne.edu"
city = "Albuquerque"
zip_code = "87101"
driver.get(url) #주어진 url로 접속
#필요한 정보를 검색하여 설정
driver.find_element(By.NAME, "name").send_keys(name)
driver.find_element(By.NAME, "email").send_keys(email)
driver.find_element(By.NAME, 'city').send_keys(city)
driver.find_element(By.NAME, 'zip').send_keys(zip_code)
driver.find_element(By.NAME, 'terms').click()
driver.find_element(By.NAME, 'submit').click()
# title의 요소를 찾고 그곳에서 thank you를 찾지 못한 경우 -1 반환
title = driver.find_element(By.ID, 'title')
assert title is not None
assert title.text.find("Thank you") >= 0
confirmation = driver.find_element(By.ID, "confirmation")
assert confirmation is not None
assert confirmation.text.find(name) >= 0
assert confirmation.text.find(email) >= 0
assert confirmation.text.find(city) >= 0
assert confirmation.text.find(zip_code) >= 0
return True
# 조건만족시 true
test_successful_order(gui_driver, httpd_url)
# True
위와 비슷한 맥락으로, 우리는 자동화된 test cases를 설정할 수 있다.
이런 경우 코드를 변경하면 자동으로 실행되어 웹 응용 프로그램이 계속 작동하도록 보장한다.
6. Retrieving User Interface Actions
- User Interface Elements
UI요소들을 찾아 할당하고 출력해보자
ui_elements = gui_driver.find_elements(By.TAG_NAME, "input")
# input태그를 가진 모든 요소를 찾아 ui_elements에 저장
for element in ui_elements:
print("Name: %-10s | Type: %-10s | Text: %s" %
(element.get_attribute('name'),
element.get_attribute('type'),
element.text))
# ui_elements의 요소들을 element에 할당
# 요소의 속성값 print()로 출력
#결과
Name: name | Type: text | Text:
Name: email | Type: email | Text:
Name: city | Type: text | Text:
Name: zip | Type: number | Text:
Name: terms | Type: checkbox | Text:
Name: submit | Type: submit | Text:
- User Interace Actions
user interface를 위한 문법을 채굴해보자
- fill(<name>, <text>) – fill the UI input element named <name> with the text <text>.
- check(<name>, <value>) – set the UI checkbox <name> to the given value <value> (True or False)
- submit(<name>) – submit the form by clicking on the UI element <name>.
- click(<name>) – click on the UI element <name>, typically for following a link.
# 예시
fill('name', "Walter White")
fill('email', "white@jpwynne.edu")
fill('city', "Albuquerque")
fill('zip', "87101")
check('terms', True)
submit('submit')
이때 상호작용(스와이프, 더블클릭 등)도 정의해야한다.
- Retrieving Actions
위에서 정의한 상호작용을 검색할 수 있도록 해보자
class GUIGrammarMiner:
def __init__(self, driver, stay_on_host: bool = True) -> None:
self.driver = driver
self.stay_on_host = stay_on_host
self.grammar: Grammar = {}
# 초기화
# stay_on_host : 웹페이지에서 호스트에 머무를지 여부를 나타내는 boolean (default: true)
gui_grammar_miner = GUIGrammarMiner(gui_driver) # 상태 및 상호작용 추출
gui_grammar_miner.mine_state_actions() # 실행
# 결과 _ 현재 웹페이지에서 가능한 action들을 반환
frozenset({"check('terms', <boolean>)",
"click('terms and conditions')",
"fill('city', '<text>')",
"fill('email', '<email>')",
"fill('name', '<text>')",
"fill('zip', '<number>')",
"submit('submit')"})
7. Models for User Interfaces
- User Interfaces as Finite State Machines
웹서버의 상태는 다음과 같다.
<order form>에서
: click시에 <terms and conditions>, click시에 <order form>으로 이동
: fill하고 submit하면 <thank you>, click시에 원 form으로 이동

- State Machines as Grammars
웹 페이지의 상호작용 시나리오는 다음과 같으며, 동작을 수행함으로써 자동화된 웹테스트를 구현할 수 있다.
<start> ::= <Order Form>
<Order Form> ::= click('Terms and Conditions') <Terms and Conditions>
fill(...) submit('submit') <Thank You>
<Terms and Conditions> ::= click('order form') <Order Form>
<Thank You> ::= click('order form') <Order Form>
- Retrieving State Grammers
현재 상태에서 user interface 문법을 추출해보자
gui_grammar_miner = GUIGrammarMiner(gui_driver)
state_grammar = gui_grammar_miner.mine_state_grammar()
#실행
state_grammar
#실행결과
# 웹 페이지 상태를 모델링하는 문법이 추출된다!
시각화하면 다음과 같다.
fsm_diagram(state_grammar)

- Executing User Interface Actions
GUIRunner를 통해서 run() method를 실행할 수 있다.
gui_driver.get(httpd_url)
gui_runner = GUIRunner(gui_driver)
#이름 지정후 실행
gui_runner.run("fill('name', 'Walter White')")

위와 유사한 과정으로 다른 요소(email, city,zipcode, checkbox 등) 들도 설정한 후 run할 수 있다.
8. Exploring User Interfaces
GUIFuzzer는 GUI를 탐색하고 상호작용하는 데에 사용되는 도구이다.
gui_fuzzer = GUIFuzzer(gui_driver, log_gui_exploration=True, disp_gui_exploration=True)
# 웹페이지에 접근 및 상호작용
# 로그로 기록o, 탐색과정을 화면에 표시x
gui_fuzzer.run(gui_runner)
# Action -> <end>
#두번쨰 실행
Action click('terms and conditions') -> <state-1>
# Action click('terms and conditions') -> <state-1> (아직 실행되지않은 상태)
In new state <state-1> frozenset({"ignore('Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.')", "click('order form')"})
# 새로운 상태 <state-1>에서 가능한 동작은 무시하거나 order form을 클릭 임을 나타냄

#세번째 실행
gui_fuzzer.run(gui_runner)
Action fill('zip', '7')
check('terms', True)
fill('name', '84')
fill('email', 'M@Chu')
fill('city', 'j')
submit('submit') -> <state-2>
In new state <state-2> frozenset({"click('order form')"})
# <state-2>에서 할 수있는 동작은 click임을 알 수있다.

9.Covering States
GrammerCoverageFuzzer은 모든 method를 탐색할 수 있게 해주며, state간의 변환을 할 수 있게 해준다.
class GUICoverageFuzzer(GUICoverageFuzzer):
def explore_all(self, runner: GUIRunner, max_actions=100) -> None:
"""Explore all states of the GUI, up to `max_actions` (default 100)."""
actions = 0
while (self.miner.UNEXPLORED_STATE in self.grammar and
actions < max_actions):
actions += 1
if self.log_gui_exploration:
print("Run #" + repr(actions))
try:
self.run(runner)
except ElementClickInterceptedException:
pass
except ElementNotInteractableException:
pass
except NoSuchElementException:
pass
자 이제 fully explore해보자
gui_driver.get(httpd_url)
gui_fuzzer = GUICoverageFuzzer(gui_driver)
gui_fuzzer.explore_all(gui_runner)
success!
fsm_diagram(gui_fuzzer.grammar)

Running the fuzzer again and again will eventually cover these expansions too, leading to letter and digit coverage within the order form.
10. Exploring Large Sites
GUI fuzzer는 사이트에서 탐색을 처리가능
: explore the first few stages
gui_driver.get("https://www.fuzzingbook.org/html/Fuzzer.html")
book_runner = GUIRunner(gui_driver)
book_fuzzer = GUICoverageFuzzer(gui_driver, log_gui_exploration=True) # , disp_gui_exploration=True)
: define n Actions
ACTIONS = 5
# 실행
# book_fuzzer.explore를 사용하여 book_runner를 통해 모든 interaction을 탐색
book_fuzzer.explore_all(book_runner, max_actions=ACTIONS)
# Run 5개의 결과가 도출된다.
Run #1
Action click('use grammars to specify the input format and thus get many more valid inputs') -> <state-7>
In new state <state-7> frozenset({"click('constraints')", "ignore('Hodov\xc3\xa1n et al, 2018')", "ignore('Grammarinator')", "ignore('Burkhardt et al, 1967')", "ignore('Backus-Naur form')", "ignore('CSmith')", "click('')", "click('coverage')", "ignore('typing')", "click('coverage-based')", "click('fuzzing functions and APIs')", "click('probabilistic grammar fuzzing')", "click('fuzz configurations')", "submit('')", "click('fuzzing graphical user interfaces')", "ignore('Hanford et al, 1970')", "ignore('Use the notebook')", "ignore('Domato')", "click('probabilistic-based')", "ignore('Last change: 2022-11-12 08:04:04+01:00')", "click('Chapter introducing fuzzing')", "click('create an efficient grammar fuzzer')", "click('The Fuzzing Book')", "ignore('Purdom et al, 1972')", "check('a58a0898-625b-11ed-9297-6298cf1a5790', <boolean>)", "ignore('copy')", "ignore('Imprint')", "ignore('inspect')", "click('basic fuzzing')", "click('Fuzzer')", "click('the GrammarFuzzer class')", "ignore('JSON specification')", "click('grammar toolbox')", "click('generator-based')", "click('Cite')", "click('use the code provided in this chapter')", "click('MutationFuzzer')", "ignore('Yang et al, 2011')", "ignore('bookutils')", "click('fuzzingbook.Grammars')", "ignore('random')", "ignore('Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License')", "ignore('re')", "click('later in this book')", "ignore('LangFuzz')", "ignore('Holler et al, 2012')", "check('a598b348-625b-11ed-9297-6298cf1a5790', <boolean>)", "ignore('Le et al, 2014')", "ignore('MIT License')", "click('"Mutation-Based Fuzzing"')", "ignore('Wikipedia page on file formats')", "click('next chapter')", "click('chapter on coverage')", "click('mutation-based fuzzing')", "ignore('Chomsky et al, 1956')", "ignore('Dak\xe1\xb9\xa3iputra P\xc4\x81\xe1\xb9\x87ini, 350 BCE')", "ignore('')", "ignore('ast')", "check('a588f304-625b-11ed-9297-6298cf1a5790', <boolean>)", "ignore('EMI Project')", "click('our chapter on coverage-based fuzzing')", "ignore('string')", "click('probabilities')"})
Run #2
Action click('use mutations on existing inputs to get more valid inputs') -> <state-18>
Run #3
Action click('The Fuzzing Book') -> <state-11>
In new state <state-11> frozenset({"click('discussed above')", "click('Sitemap')", "click('fuzzingbook.Fuzzer')", "ignore('os')", "check('e1cd81d4-6ff0-11ed-9dea-6298cf1a578e', <boolean>)", "click('')", "ignore('typing')", "ignore('XKCD comic')", "click('chapter on information flow')", "click('A Fuzzing Architecture')", "click('About this book')", "click('reduce failing inputs for efficient debugging')", "submit('')", "ignore('tempfile')", "click('use grammars to specify the input format and thus get many more valid inputs')", "click('IV\nSemantical Fuzzing')", "ignore('LLVM Address Sanitizer')", "ignore('Use the notebook')", "click('Intro_Testing')", "ignore('assignment')", "click('runtime verification')", "click('chapter on testing')", "click('The Fuzzing Book')", "click('V\nDomain-Specific Fuzzing')", "ignore('Imprint')", "click('"Introduction to Software Testing"')", "click('Cite')", "click('II\nLexical Fuzzing')", "click('use the code provided in this chapter')", "click('chapter on mining function specifications')", "ignore('bookutils')", "ignore('random')", "click('VI\nManaging Fuzzing')", "ignore('Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License')", "ignore('Python tutorial')", "ignore('HeartBleed announcement page')", "ignore('red-black tree')", "ignore('MIT License')", "ignore('Last change: 2022-07-25 12:07:43+02:00')", "check('e1d4c994-6ff0-11ed-9dea-6298cf1a578e', <boolean>)", "ignore('HeartBleed bug')", "click('Index (beta)')", "click('Introduction to Testing')", "ignore('')", "click('I\nWhetting Your Appetite')", "ignore('MyPy')", "ignore('subprocess')", "ignore('Takanen et al, 2008')", "click('ExpectError')", "ignore('Miller et al, 1990')", "click('Appendices')", "click('III\nSyntactical Fuzzing')", "click('use mutations on existing inputs to get more valid inputs')"})
Run #4
Action click('Cite') -> <state-13>
Run #5
Action click('runtime verification') -> <state-9>
5번의 과정을 통해서 각 state와 action정보를 알 수 있어 이를 통해 다양한 test case를 생성하거나
시스템 동장을 모의실행 하는 등의 활용이 가능하다.
'System Hacking > Fuzzer' 카테고리의 다른 글
| fuzz APIS (1) | 2023.05.25 |
|---|