17 ก.พ. 2558

[Tutorial] Performance test web application that needs credentials using JMeter (Windows platform)

บทความนี้บัวบานจะกล่าวถึงการทำ Performance test Web application ที่มีการ authenticate โดยตัว web application เองนะฮะ. ในอนาคตจะมีบทความใหม่เกี่ยวกับ authenticate ด้วย windows authentication.

โดยปกติแล้วการ authenticate ด้วยตัว application เองนั้นมักจะใช้วิธีใส่ username และ password ผ่าน web form แล้วให้ user submit form นั้น, ต่อมาฝั่ง application บน server ก็จะตรวจสอบตัวตน (Authenticate) และตรวจสอบสิทธิ์ (Authorization), ผลลัพธ์สุดท้าย ฝั่ง application บน server จะมอบ token กลับมาหนึ่งชุด (token ก็คือ string อะไรสักอย่างหนึ่ง)  ให้ web browser ของ client เก็บไว้ โดยมักจะจัดเก็บ token นั้นไว้ใน cookies.

Workflow of Testing

Figure 1 Workflow ของการทำ Performance Test Web Application ที่ต้องการ credentials

Figure 1 แสดง Workflow ของการทำ performance test web application ที่ทำ authenticate โดยตัว web application เอง. แบ่งเป็นขั้นตอนดังนี้
  1. Open Login page and Extract VIEWSTATE
    จุดประสงค์เพื่อเก็บ token บางอย่างที่อาจจะต้องใช้ในการ submit login form เช่น viewstate. เราต้องให้ JMeter ส่ง request ไปที่ URL ของ Login page, แล้วเก็บค่าที่ต้องการไว้ในตัวแปร.
  2. Submit login form
    ส่วนนี้เป็นการส่ง username และ password ไปให้ฝั่ง application บน server. Response มักจะเป็น token พร้อมกับ URL ของ landing page.
  3. Navigate to landing page
    ขั้นตอนนี้เป็นการทดสอบการใช้ token ที่เราได้มา ว่าสามารถเข้าถึง web page หรือ service ที่ต้องการ credential ได้.
  4. Start requesting data
    เริ่มขั้นตอนการทดสอบจริงๆ.

บัวบานจะยกตัวอย่างการ Search flight ของ airasia
Login page: https://member.airasia.com/login.aspx?culture=th-TH
Landing page: https://member.airasia.com/profile-landing.aspx

Test Plan Development

Structure of test plan

บัวบานนำเสนอ structure สองแบบ.

1st Choice - Simple plan

ถ้าจะทำแบบง่ายๆใช้เร็วๆ บัวบานแนะนำให้มี Thread Group อันเดียว, เพื่อความง่ายในการจัดการ cookies. Figure 2 แสดง structure ของ Test Plan แบบ simple.
Figure 2 Structure ของ Test Plan แบบ Simple

ภายใน Thread Group จะประกอบด้วย

  1. HTTP Cookie Manager
    เมื่อเราใส่ HTTP Cookie Manager เข้าไปใน Thread Group, JMeter จะจัดการ cookie ให้โดยอัตโนมัติ. ซึ่ง HTTP Cookie Manager นี้จะมีผลภายใน Thread Group เดียวเท่านั้น. (แต่บางทีก็แชร์ข้าม Thread Group ได้. คาดว่าเป็นบั๊กนะฮะ).
    วิธีการเพิ่ม HTTP Cookie Manager ก็เพียงแค่คลิกขวาที่ Thread Group, เลือก Add > Configure Element > HTTP Cookie Manager.
  2. HTTP Request Defaults
    เอาไว้กำหนดค่า default ให้การทำ HTTP Sampler เช่น Server name/IP, Port, Proxy name/IP, ...
  3. Loop Controller สำหรับขั้นตอนการ login
    ใช้สำหรับกำหนดจำนวนครั้งในการ login.​โดยเราจะกำหนดให้ทำเพียง 1 ครั้ง. ภายใต้ Loop Controller นี้จะประกอบไปด้วย:
    - HTTP Sampler เพื่อใช้สำหรับการเปิดหน้า login และ submit login form
    - Regular Expression Extractor เพื่อใช้เก็บค่าบางอย่างจาก response ที่ได้กลับมา
    หลังจาก loop นี้ทำงานเสร็จ, ใน cookies ของเราจะมี token ที่ได้จากการ login, ซึ่งเป็นเปรียบเสมือนบัตรผ่านสำหรับเข้าใช้ web application.
  4. Loop Controller สำหรับ performance test
    ใช้สำหรับกำหนดจำนวนครั้งของการยิง request เพื่อทำ performance test.

2nd Choice - Advanced plan

ในกรณีที่การทำ performance test นั้นมีส่วนประกอบเยอะ เช่น ต้องสร้าง background load ให้ server หรือมีการเรียกหลายๆ service ที่ authenticate/authorize ต่างกัน, บัวบานแนะนำให้สร้าง Thread Group แยกสำหรับแต่ละ service ไป, และควรแยก  Group สำหรับ Authenticate/Authorize ด้วย.
Figure 3 แสดง structure ของ Test Plan แบบ Advance.
Figure 3 Structure ของ Test Plan แบบ Advance


ภายใน  Test Plan จะประกอบด้วย
  1. Setup Thread Group
    Setup Thread Group คือ Thread Group ที่จะถูก execute แรกสุดเลย, และ Thread Group ปกติจะเริ่มทำงานหลังจากที่ Setup Thread Group ทำงานจนจบแล้ว. ภายใน Thread นี้จะประกอบไปด้วย HTTP Cookie Manager, HTTP Request Defaults, HTTP Sampler, BeanShell PostProcessor.
    BeanShell คือภาษา Java แบบ script, ไม่ต้อง compile ก็รันได้เลย. เราสามารถเขียน BeanShell script สำหรับ handle event ต่างๆที่เกิดขึ้นภายใน JMeter ได้.
    BeanShell PostProcessor คือ BeanShell script ที่จะถูก execute หลังจาก sample เสร็จแล้ว. ถ้าเราเอา BeanShell PostProcessor ใส่ไว้ใน HTTP Request, BeanShell script นั้นจะถูก execute หลังจากที่ sampler ได้รับ response กลับมาจาก server, ซึ่งทำให้เราสามารถประมวลผล response นั้นแล้วนำมาเก็บใส่ตัวแปรเพื่อใช้ต่อในอนาคตได้.
  2. Thread Group สำหรับ performance test. ภายใน Thread Group จะต้องมี BeanShell PreProcessor เพื่อใช้เขียน token ที่ได้จาก Setup Thread Group ลงใน cookies ของ Thread นี้ก่อนที่จะเริ่มทำการ sample.


Login

สิ่งแรกที่เราต้องรู้ก็คือ เราต้อง submit login form ไปที่ URL อะไร?
วิธีที่บัวบานนิยมใช้ก็คือเปิด Google developer tools (dev tool) ขึ้นมา แล้วไปยัง tab Network. จากนั้นไปที่หน้าเว็บและกรอก username, password แล้วกด login. กลับมาที่ dev tool เพื่อดูการส่งข้อมูลไปยัง server. ไล่กดดูทีละตัวจนกว่าจะพบ request ที่มีการ submit form ซึ่งมี username และ password ที่เรากรอกเข้ามา​.
พอเจอแล้ว ก็ให้ตรวจดูว่าเราจะต้องส่งอะไรเข้าไปใน login form บ้าง.
จากรูป เราจะพบการ submit form ไปที่ https://member.airasia.com/login.aspx?culture=th-TH
Figure 4 Form Data ที่ถูกส่งไปเมื่อ user กด login

Figure 4 แสดงรายการ requests ที่เกิดขึ้นเมื่อ user กดปุ่ม login. วงกลมสีเขียวซ้ายมือ แสดงชื่อของ resource ที่ request, วงสีเขียวขวามือคือ Form Data, วงสีแดงคือ username และ password ที่เรากรอกเข้ามาในหน้าเว็บเมื่อสักครู่.
ภายใน Form Data จะเห็น variable 4 ตัวที่มีค่าคือ __VIEWSTATE, ctl00$body$txtUsername, ctl00$body$txtPassword, และ ctl00$body$btnLogin. และจะมี variable ที่มีค่าเป็นว่างๆ อีก 3 ตัวคือ __EVENTTARGET, __EVENTARGUMENT, และ __VIEWSTATEENCRYPTED.

การหาค่า VIEWSTATE

ขั้นตอนถัดไป เราจะต้องหาที่มาของ VIEWSTATE.
โดยทั่วไป VIEWSTATE นี้จะตรงกับชื่อของ <input> ซักอันใน html ของหน้า login. ดังนั้นเราต้องเปิดหน้า login อีกครั้งหนึ่ง (อาจจะใช้ incognito window) แล้ว view page source หรือใช้ Inspect element ก็ได้ แล้วมองหาคำว่า VIEWSTATE ใน HTML. Figure 5 แสดงตำแหน่งของ __VIEWSTATE ใน HTML.
Figure 5 VIEWSTATE ภายใน HTML
ถ้าไม่พบ ให้พยามมองหาสิ่งของที่มีค่าตรงกับค่าที่ส่งไปใน Form Data. (แต่ค่า __VIEWSTATE นี่จะเปลี่ยนทุกครั้งที่เปิดหน้า login ใหม่นะฮะ).
เมื่อได้ที่มาของ __VIEWSTATE แล้วให้กลับไปที่ JMeter > Open Login Page sampler, click ขวา แล้วเลือก PostProcessor > Regular Expression Extractor.

จากนั้นใส่ค่าให้ parameter ต่างๆ ตามรายการข้างล่าง. Figure 6 แสดง Regular Expression Extractor.
Figure 6 การตั้งค่า Regular Expression Extractor
  • Reference Name: เป็นชื่อตัวแปร เราตั้งได้ตามใจชอบ. บัวบานขอตั้งชื่อว่า VIEWSTATE.
  • Regular Expression: เป็น Expression ที่ใช้หาและดึงเอาค่าของ __VIEWSTATE ใน HTML. ในกรณีของ airasia นี้ให้ใช้ expression:
    id="__VIEWSTATE" value="(.+?)"
    ภายในวงเล็บ (.+?) คือค่าที่เราจะดึงมา โดยจะถูกเก็บไว้ในตัวแปร $1$. ในกรณีที่มีวงเล็บหลายๆที่เพื่อดึงค่าจากหลายที่ ชื่อตัวแปรก็จะเรียงตามลำดับ $2$, $3$, ...
  • Template: เป็น Template ของค่าที่จะถูกเก็บไว้ในตัวแปร VIEWSTATE ที่เราตั้งไว้. ในกรณีนี้คือค่าที่ได้จากการ match ครั้งที่ 1 ซึ่งก็คือ $1$. 
  • Match No.: ... ไม่รู้จะอธิบายยังไงดี ฮ่ะๆ.
  • Default Value: ค่าตั้งต้นของตัวแปร.

Submit Login

HTTP Sampler ตัวนี้จะทำหน้าที่ submit login จริงๆ. โดยเราต้องใส่ค่าให้ parameter 4 ตัว ดังนี้:

  1. __VIEWSTATE: ${VIEWSTATE}
  2. ctl00$body$txtUsername: username ของเรา
  3. ctl00$body$txtPassword: password ของ user เรา
  4. ctl00$body$btnLogin: %E0%B8%A5%E0%B9%87%E0%B8%AD%E0%B8%81%E0%B8%AD%E0%B8%B4%E0%B8%99%E0%B9%80%E0%B8%82%E0%B9%89%E0%B8%B2%E0%B8%AA%E0%B8%B9%E0%B9%88%E0%B8%A3%E0%B8%B0%E0%B8%9A%E0%B8%9A
    อันนี้เป็น code ของภาษาไทยคำว่า "ล็อกอินเข้าสู่ระบบ"
  5. __EVENTTARGET:
  6. __EVENTARGUMENT:
  7. __VIEWSTATEENCRYPTED: 
  8. ctl00$body$chkRememberMe: on


เป็นอันเสร็จสิ้นการ Login แบบ simple plan. ให้ลอง Add Listener > View Result In Tree, แล้วกด Start เพื่อเริ่มต้นการเทสต์. เมื่อกดดูที่ URL ภายใต้ sampler Submit Login form ก็จะเห็น token อยู่ใน Cookie ดัง Figure 7.
Figure 7 Token ที่ได้หลังจาก Submit Login form

ณ จุดนี้ Test ที่มี structure แบบ simple plan ก็จะมี token ให้ใช้สำหรับผ่านเข้าออกหน้าที่ต้องการ credentials ได้แล้ว. โดยที่ HTTP Cookie Manager จะคอยจัดการเพิ่ม cookie ให้ sampler ภายใน Thread Group โดยอัตโนมัติ.

แต่ทว่า structure test plan แบบ advance ยังไม่จบเท่านี้. เนื่องจาก HTTP Cookie Manager นั้นจะจัดการ cookie ภายใน Thread ของตัวเองเท่านั้น. นั่นหมายความว่า เราต้องหาทางแชร์ Cookie จาก Setup Thread Group มาให้ Thread Group อื่นๆ.
เราจะใช้ BeanShell Pre/Post Processor เข้ามาช่วย โดยให้ BeanShell PostProcessor ภายใน Setup Thread Group เขียนค่าของ cookies ลงในตัวแปรหรือ JMeter Properties, จากนั้นใช้ BeanShell PreProcessor อ่านค่านั้นแล้วเขียนใส่ Cookie ของ HTTP Sampler ภายใน Thread Group, แล้วปล่อยให้ HTTP Cookie Manager ภายใน Thread Group นั้นจัดการค่า cookie ต่อไป.

การอ่านเขียนค่า Cookies ด้วย BeanShell Processor

มี 2 แนวทางที่จะอ่านเขียนค่า cookies. วิธีแรกคืออ่านเขียนเฉพาะ cookie ที่เราต้องการ, วิธีที่สองคืออ่านเขียนทุกค่า.

อ่าน/เขียนเฉพาะ cookie ที่ต้องการ ผ่านตัวแปร COOKIE_ชื่อcookie

อันดับแรก, เราต้องตั้งค่าให้ JMeter ให้ save ค่าของ cookies ลงตัวแปร, โดยเข้าไปหา config file ตัวหนึ่งที่ folder ของ JMeter > bin > jmeter.properties, เมื่อเจอแล้วก็ให้ edit ด้วย notepad. จากนั้นใน search หา "CookieManager.save.cookies=false" แล้วเปลี่ยนให้เป็น CookieManager.save.cookies=true ดัง Figure 8.
Figure 8 JMeter Properties สำหรับ save cookies เป็น variable
จากนั้นเราจะสามารถอ่านค่าแต่ละ cookie ภายใน cookies ได้จากตัวแปรที่ชื่อ "COOKIE_ชื่อcookie". ยกตัวอย่างเช่น ใน cookies ของ airasia member นี้มี cookie ชื่อ ticket (ที่เป็น token ยาวๆ), เราสามารถเข้าถึงค่านี้ได้จากตัวแปรชื่อ COOKIE_ticket.

อ่าน/เขียนทุก cookies

ในกรณีที่ cookies ที่ต้องใช้มันมีหลายค่า และเราไม่รู้ว่าจะมีการเปลี่ยนแปลงอะไรในอนาคตรึเปล่า, ก็ควรใช้วิธีนี้นะฮะ. วิธีการนี้จะวนลูปอ่านค่าจาก HTTP Cookie Manager ใน Setup Thread Group แล้วเขียนค่าลงใน JMeter Property. จากนั้นในแต่ละ Thread Group ก็จะอ่านค่าจาก JMeter Property แล้วเขียนค่านั้นลงใน cookies.

Code สำหรับ BeanShell PostProcessor เพื่ออ่าน cookies แล้วเขียนค่าลง JMeter Property


import org.apache.jmeter.protocol.http.control.CookieManager;

CookieManager mgr = ctx.getCurrentSampler().getProperty("HTTPSampler.cookie_manager").getObjectValue();

props.put("CookieCount", String.valueof(mrg.getCookieCount()));

for(int i=0; i < mgr.getCookieCount(); i++) {
 props.put("cookie_name_" + i, mgr.get(i).getName());
 props.put("cookie_value_" + i, mgr.get(i).getValue());
 props.put("cookie_domain_" + i, mgr.get(i).getDomain());
 props.put("cookie_path_" + i, mgr.get(i).getPath());
 props.put("cookie_secure_" + i, mgr.get(i).getSecure());
 props.put("cookie_expires_" + i, mgr.get(i).getExpires());
}



//props.put("COOKIE1","${COOKIE_userLogin}");
//props.put("COOKIE2","${COOKIE_ticket}");
//props.put("COOKIE3","${COOKIE_firstnm}");
//props.put("COOKIE4","${COOKIE_lastnm}");
//props.put("COOKIE5","${COOKIE_memberLogin}");

Code สำหรับ BeanShell PreProcessor เพื่ออ่าน JMeter Property แล้วเขียนค่าลง cookies
import org.apache.jmeter.protocol.http.control.CookieManager;
import org.apache.jmeter.protocol.http.control.Cookie;

int count = Integer.ParseInt(props.getProperty("CookieCount"));
CookieManager manager = sampler.getCookieManager();

for(int i=0; i < count;i++) {
 props.put("cookie_name_" + i, mgr.get(i).getName());
 props.put("cookie_value_" + i, mgr.get(i).getValue());
 props.put("cookie_domain_" + i, mgr.get(i).getDomain());
 props.put("cookie_path_" + i, mgr.get(i).getPath());
 props.put("cookie_secure_" + i, mgr.get(i).getSecure());
 props.put("cookie_expires_" + i, mgr.get(i).getExpires());
 
 Cookie cookie = new Cookie(props.get("cookie_name_" + i), props.get("cookie_value_" + i),props.get("cookie_domain_" + i),props.get("cookie_path_" + i),props.get("cookie_secure_" + i),props.get("cookie_expires_" + i));
 manager.add(cookie);
}

ท่านผู้ชมสามารถ download ตัวอย่าง Test Plan ทั้งสองแบบได้จาก
Simple Plan: https://drive.google.com/file/d/0B69Rt-ghTQqyYk45NWpWZFM0dms/view?usp=sharing
Advance Plan: https://drive.google.com/file/d/0B69Rt-ghTQqyalgzeXNhb1EzYWs/view?usp=sharing