Cảm ơn bạn đã đọc và ủng hộ blog KTMT ʘ‿ʘ Từ bây giờ chúng tôi sẽ là kipalog.com !

Comments

Mở đầu

Trong quy trình phát triển một phần mềm, Test nói chung và UT nói riêng luôn là những process rất quan trọng. Tuy nhiên khi Application càng ngày càng lớn thì khối lượng Test càng phình to và cost sẽ vượt quá cost bỏ ra cho coding. Để giảm thiểu số công sức bỏ ra, developer thường hay dùng các test framework có sẵn và tìm cách automation quy trình test.

PHP có PHPUnit, Java có JUnit, Python có nose v.v… Tuy nhiên bên ngoài UT vẫn còn những quy trình bắt buộc phải làm bằng tay. Bạn viết ra 1 website, bạn muốn test trên website bạn có đúng những link hiện ra như bạn muốn hay ko, màu sắc có thay đổi hay không, khi user click vào link có nhảy đến page target và mang theo hàm callback đã định nghĩa hay ko v.v…

Trong bài viết này sẽ giới thiệu 2 công cụ automation UT và test khá nổi tiếng. Sủ dụng và tận dụng, đôi khi sẽ thành những tool mang lại hiệu quả bất ngờ trong cả những công việc khác :D

Selenium

Selenium - nói 1 cách đơn giản, là công cụ automator tất cả các thao tác của con người trên browser. Bạn có thể giả lập 1 set các action, VD như: < User bật browser lên, User vào trang web của bạn, User đi theo link route linh1-link2-link3-link4, User click vào download button trong link 4 >

Selenium có 2 cách dùng, Selenium Server và Selenium WebDriver. Hiện nay trên firefox đã có plugin Selenium IDE. Bạn có thể dùng nó để record lại hành động của mình và export ra test code in Python, Ruby, C#, hoặc Java. Selenium test code chạy ở chế độ bình thường sẽ tự động bật browser của bạn lên và tự hành động y như bạn đã làm trước dó :D

Tuy nhiên để quy trình test không tốn quá nhiều tài nguyên, người ta thường hay config để selenium chạy headless trên Linux. VD như Python sẽ có package Xvfb, cho phép tạo display chạy ngầm và bật web driver trên đó.

Ở dưới là VD config cho CentOS distro

1
2
3
4
pip install selenium
yum install Xvfb 
yum install firefox
pip install pyvirtualdisplay
~/.bash_profile
1
2
3
4
...
#Create virtual screen by Xvfb
Xvfb :5 -ac -screen 0 1024x768x8 &
...
TestMySite.py
1
2
3
4
5
6
7
8
from selenium import webdriver
...
class SeleniumHeadlessTest(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Firefox()
        self.base_url = "" #Testing URL here 
        self.verificationErrors = []
....
running python test
1
2
export DISPLAY=:5.0
python TestMySite.py

Bypass the working attendance system

Ở phần này tôi sẽ đặt ra 1 ví dụ trực quan: giả sử, công ty bạn có hệ thống check time làm việc của nhân viên. Nhân viên khi đến công ty phải login vào page, ấn vào button “Đã đến”. Tương tự khi về lại phải login vào và ấn thêm button “Đã về”

Có selenium trong tay, bạn có thể giải quyết vấn đề khá đơn giản, viết 1 test script mô phỏng toàn bộ quá trình nói trên, đặt cron cho nó chạy vào giờ đến và giờ về chỉ định

selenium - selenium.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from selenium import webdriver
...
class SeleniumHeadlessTest(unittest.TestCase):
    def setUp(self):
        mock= ""
        self.driver = webdriver.Firefox()
        self.base_url = mock
        self.verificationErrors = []

    def test_in(self):
        name=""
        pwd=""
        driver = self.driver
        driver.get(self.base_url)
        driver.find_element_by_xpath("..._name_field_").send_keys(name)
        driver.find_element_by_xpath("..._password_field_").send_keys(pwd)
        driver.find_element_by_xpath("..._login_button_").click()
...

Thêm 1 ít sleep time random(VD random trong khoảng 10 phút) và đặt cron 30 10 * * 1-5. Bạn đã có 1 con bot luôn check time in cho bạn trong khoảng 10h30-10h40 mỗi ngày trong tuần :D Với package Xvfb như đã nói ở trên, con bot sẽ chạy silent bên trong máy (hay máy ảo) và ko tạo ra 1 notice nào. Redirect log ra 1 file ẩn sẽ giúp manage quá trình chạy tốt hơn.

Khi xuất hiện 1 số ngày nghỉ ko phải thứ 7, CN mà muốn đặt lịch để con bot ko chạy, vấn đề sẽ hơi phức tạp hơn 1 chút. bạn cần phải viết 1 con bot observer khác check ngày và update lại cron job

Selenium + Nose

Nếu bạn đã dùng Python 1 thời gian, chắc hẳn sẽ biết đến Nose - UT framework cho Python. Ở phần này sẽ demo thêm 1 config cho Selenium + Nose

Yêu cầu: Trang web www.mysite.com của bạn có 100 sublink: www.mysite.com/1 , www.mysite.com/2, www.mysite.com/3 ….. www.mysite.com/100

Bạn phải test xem trong từ sublink đó có 1 file image tên là “myimage.png” với div=myimage hay không.

Như vậy có 100 test case ở đây, tuy nhiên content khá giống nhau, bạn có thể nghĩ đến chuyện tạo template

template.py
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
from selenium import webdriver
from selenium import unittest
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException

class SeleniumHeadlessTest(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Firefox()
        self.base_url = "www.mysite.com"
        self.failures = []

    def Test(self):
        driver = self.driver
        driver.get(self.base_url+ "foo")
        try: driver.find_element_by_xpath("//div[@id='myimage']")
        except NoSuchElementException:
            driver.back()
            self.failures.append("foo")
        self.assertEqual(len(self.failures),0)

    def tearDown(self):
        self.driver.quit()

if __name__ == "__main__":
    unittest.main()

Tạo file config chứa các giá trị muốn replace cho “foo” trong template.py, ở đây cụ thể là 1..100

config
1
2
3
4
5
6
7
# any comment
! another comment
$ I'm comment too, do u believe me :D
1
2
...
100

Code 1 cái test_generator, ở đây tôi dùng chính Python fabric:D

selenium - fabfile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# pip install fabric
from __future__ import with_statement
from fabric.api import *
import sys, os, fileinput, textwrap

_DIR=os.getcwd()

def test_generator():
    with open('./config','r') as f:
        # Ignore comment line in config file
        prefs=["#","!","<","/","$","%","&","\n"]
        ffilter = filter(lambda item: not any(item.startswith(prefix) for prefix in prefs), f.readlines()
        for line in ffilter:
            bar=line.strip()
            with lcd(_DIR):
                # create test source 
                local('cp -r template.py '+bar+'.py')
            with open('./'+bar+'.py','r+') as tf:
                # replace all "foo" with variable read from config file
                tfmap = map(lambda l: l.replace("foo","bar") if "foo" in l else l, tf.readlines())
                tf.seek(0)
                tf.writelines(tfmap)
            tf.closed
    f.closed

Done! Giờ bạn có thể

1
2
3
4
#pip install multiprocessing
export DISPLAY=:5.0
fab test_generator 2>fabErr.log
nosetest --exe -v --process=4 --process-restartworker .

Jenkins

Tiếp tục về quy trình test automation, Jenkins là 1 tool xuất sắc khác. Jenkins đại diện cho khái niệm CI (continuous integration).

Bạn muốn cả bộ UT/test chạy mỗi ngày vào 10 h sáng. Mội ngày system leader đến check log để biết có bao nhiêu test đang fail, bao nhiêu error, quản lý health theo từng ngày, bạn hoàn toàn có thể dùng test framework + cronjob

Tuy nhiên Jenkins cung cấp 1 giao diện trực quan hơn, percentage, graph, coverage report, code convention checking ,v.v….. hoàn toàn tự động, có thể wake up theo svn hoặc git. Jenkins cũng có rất nhiều plugin và support hầu hết các test framework.

VD về Jenkins jobs

images Source Code Management

images Execute Shell

images Build Trigger

Đặt lịch, tương tự cron jobs

1 recommended config là tạo 1 jenkins job observer check svn và git, nếu có update tại subsystem nào thì run jenkins jobs tương ứng với subsystem đấy. 1 sub system lại có thể chứa nhiều test framework cần wakeup (VD với PHP + Oracle, bạn có PHPUnit và utPLSQL)

Kết luận

  • Cron jobs: package lập lịch của Linux

  • Xvfb: package tạo display ảo của Linux

  • Selenium: là công cụ giả lập action trên browser, bắt chước y hệt thao tác của user và ko loại trừ bất cứ hạn chế nào

  • Jenkins: CI Test Tool, dùng để lập lịch, theo dõi và quản lý health của system

Comments

I.Script loader là gì và tại sao lại cần nó

Trong javascript, khi cần include một thư viện, hay một module từ ngoài vào, chắc hản mọi web developer đều nghĩ ngay đến việc include vào html:

include direct - include.html
1
 <script src="http://yourhost/script.js" ></script>

Vậy include trực tiếp script tag vào html có gì không tốt?

  • Block việc render GUI của web browser: Cơ chế render của web browser là render tuần tự, đi từ trên xuống dưới.Chính vì thế mà khi gặp script tag thì đầu tiên là web browser phải download về, sau đó parse và execute script đó , sau đó mới render những thứ tiếp theo. Việc này làm cho việc render nội dung web sẽ bị block lại . Thử hình dung bạn sử dụng thư viện ember.js, thư viện này sau khi minified lại có dung lượng khoảng 200kb, bạn download từ cdn về mất 1.5s, bạn render mất 0.5s nữa, tổng cộng đã mất 2s, là một con số không nhỏ.

  • Khi qui mô của web lớn lên, đặc biệt tại thời điểm mà các framework mvc cho js nở rộ như hiện nay với ember, backbone hay angular và việc phát triển bùng phát của single-page web app(những application viết chủ yếu bằng javascript) thì việc quản lý chặt chẽ thư viện, module nào có dependency ra sao, nên được load vào thời điểm nào là hết sức quan trọng

Để giải quyết vấn đề đó, thì chúng ta sẽ sử dụng một khái niệm gọi là script loader. Script loader chỉ đơn giản là chuyển việc load script từ html vào một cái script js chỉ chuyên làm nhiệm vụ “load” các dependent scripts. “load” bằng cách nào thì rất đơn giản, chỉ là tạo ra một script tag, gán source và insert vào dom. Việc này khác việc include script bằng html là nó không block UI, nó chỉ đơn thuần là request đến server chứa script cần load thông qua XHR, lấy kết quả về, và eval đoạn script đó.

Ví dụ về script loader:

script loader demo - loader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
(function(){
  var scList = new Array();
  scList[0] = 'http://cdnjs.cloudflare.com/ajax/libs/ace/0.2.0/ace.js';
  scList[1] = 'http://cdnjs.cloudflare.com/ajax/libs/alloy-ui/1.0.1/aui-min.js';
  scList[2] = 'http://cdnjs.cloudflare.com/ajax/libs/barman/0.2.2/barman.min.js';
  var len = scList.length;
  for (var i=0; i<len; i++) {
    var script = document.createElement('script');
    var firstScript = document.getElementsByTagName('script')[0];
    script.src = scList[i];
    firstScript.parentNode.insertBefore(script, firstScript);
 }
})()

Và kết quả đạt được là:

  1. Load script trực tiếp vào html tag images

  2. Load script thông qua loader images

Có được kết quả trên là vì đưa việc loading js vào trong script giúp cho ta có thể load các module đó asynchronousi thông qua XHR(ajax), và nhờ đó rút ngắn được thời gian load + render Như vậy là ta đã giải quyết được bài toán thứ nhất, tuy nhiên có một vấn đề là để load được một module thông qua script loader thì module đó bắt buộc phải tuân theo một qui chuẩn nào đó để giúp qui định về thứ tự load, và dependency. Để giải quyết vấn đề đó, đồng thời cũng để giải quyết vấn đề thứ hai đã nêu ở trên chúng ta sẽ đưa ra khái niệm AMD

II AMD

AMD là viết tắt của Asynchronous Module Definition, là một qui chuẩn của javascript dành cho việc load các script/module và các dependency của chúng từ ngoài vào một cách không đồng bộ (asynchronously).

Thực tế gọi là một qui chuẩn, nhưng AMD chỉ đơn thuần qui định 2 rule cơ bản:

  • Interface cho hàm define()
define define.js
1
 define(id?, dependencies?, factory);
  • param id: qui định id của module được load vào, [?] là do param này là optional, có thể bỏ qua
  • param dependencies: là 1 array các module dependency của module được load vào, param này cũng là optional
  • param factory: là đoạn script dùng để initialze cho module sẽ được load vào. factory() sẽ chỉ được execute một lần , và nếu factory() có return value thì return value này nên được export ra ngoài để có thể sử dụng lại ở trong các script khác

Một ví dụ đơn giản cho AMD interface:

amd if - amd.js
1
2
3
4
5
6
7
define(["alpha"], function (alpha) {
  return {
    verb: function(){
      return alpha.verb() + 2;
    }
  };
});
  • Property amd cho hàm define: Function define nên có property tên là amd. Việc này giúp tránh conflict khi module của bạn đã có một function tên là define, và trong property này sẽ định nghĩa là module của bạn có cho phép nhiều version trên cùng một document không ( khi module của bạn đã conform theo AMD, thì chắc chắn trong hàm require phải có đoạn check là đã có property này hay chưa và check giá trị của nó).
amd - amd.js
1
2
3
define.amd = {
  multiversion: true
};

Nói đến đây thì chấc sẽ có bạn thắc mắc, hoặc chưa hiểu rõ use case của cái AMD này như thế nào, nó được dùng ở đâu, ở script loader, ở module, hay ở dom. Câu trả lời là AMD sẽ được dùng ở script loader và ở module. Cụ thể hơn là trong module của bạn, nếu bạn muốn module đó được load async thông qua script loader, mà script loader đó lại load theo chuẩn AMD, thì đương nhiên module của bạn cũng sẽ phải conform theo AMD, bằng cách là có hàm define() trong module, và có property amd của hàm define. Còn script loader bản thân cũng là một module, thì tất nhiên cũng phải tuân theo AMD.

Một cách ngắn gọn, giả sử bạn có một module X

module - module.js
1
2
3
4
X = (function() {
  var prop = {};
  return prop;
})()

Bạn muốn module đó nói với bên ngoài là: tao lã X, tao có các dependency là Y, Z, khi init tao thì mày làm thế này, thế này nhé thì bạn sẽ làm theo AMD api theo cách như sau:

module with amd module.js
1
2
3
4
5
6
7
8
9
10
X = (function() {
  var prop = {};
  prop.define = function(name, deps, callback) {
  }

  callback = function() {//do something to init here}
  prop.define.amd = {multiversion: true}

  return prop;
})()

Và khi script loader nhìn vào cái define của bạn, nó sẽ biêt nên làm thê nào. Rất đơn giản phải không.

III Các scriptloader nổi tiếng và việc áp dụng AMD đang ở đâu

Hiện nay, có một số script loader nổi tiếng như:

  • YepNope: http://yepnopejs.com/
  • RequireJs: http://requirejs.org/docs/
  • Headjs: https://github.com/headjs/headjs
  • CurlJs: https://github.com/cujojs/curl Ngoài ra trong bộ toolkit nổi tiếng Dojo cũng có sử dụng script loader

Trong những script ở trên thì có requirejs và curljs là sử dụng AMD, còn lại 2 script còn lại là yepnope và headjs thì không. Về số lượng được sử dụng nhiều nhất thì có lẽ là requirejs.

Hiện tại các module nổi tiếng thì không phải module nào cũng conform theo AMD. Theo mình biết thì hiện tại có jQuery là support AMD internally, còn lại thì phần nhiều các module nổi tiếng khác như backbone, ember, angular đểu không support AMD internally. Để sử dụng các module này với một script loader theo chuẩn AMD như require.js thì bạn đơn giản chỉ cần viết lại hàm define tại app của bạn, ví dụ như trong trường hợp của backbone:

backbone with amd - bbamd.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require.config({
  paths: {
    jquery: 'libs/jquery/jquery',
    underscore: 'libs/underscore/underscore',
    backbone: 'libs/backbone/backbone'
});

require([
    // Load our app module and pass it to our definition function
    'app',
    ], function(App){
      // The "app" dependency is passed in as "App"
      App.initialize();
    }
);

Vậy tại sao AMD có rất nhiều merit như thê mà một số module nổi tiêng lại bỏ qua việc conform theo AMD, ví dụ tiêu biểu nhất là emberjs. Theo như Tom Dale, một trong những creator của emberjs thì AMD yêu cầu quá nhiều HTTP request, bởi vì để conform theo AMD thì script phải chia ra thành nhiều module, nhiều file. Ngoài ra thì AMD cũng yêu cầu toàn bộ module phải wrap trong một function (factory()), việc này có thể ok với một số người nhưng cũng sẽ gây khó chịu với một số người khác. Và cuối cùng là một số build tool hiện tại (ví dụ như Grunt https://github.com/cowboy/grunt) hỗ trợ rất tốt cho việc quản lý dependency và version rồi, thế nên việc conform cấu trúc code của mình theo một cái có sẵn như AMD là không cần thiết.

IV Kết luận

Script loader đã và đang trở thành một kĩ thuật không thể thiếu trong việc tạo ra một responsive web app, giúp rút ngắn thời gian load và render js. Cộng với việc AMD ra đời chúng ta đang thấy ecmascript, cụ thể hơn là javascript đang có những nỗ lực trở nên mature hơn, để có thể trở thành ngôn ngữ mà developer có thể cảm thấy thoải mái khi phát triển và khi scope của application bị phình to ra. Tại version ecma hiện tại (ECMA-262) thì vẫn chưa có một qui chuẩn nào cho việc load script theo module và dependency, tuy nhiên chúng ta có thể hy vọng về điều này trong một thời gian gần.

Comments

Mở đầu

Khi học C cơ bản, chắc hẳn bạn sẽ gặp cách dùng từ khoá static như ví dụ dưới đây:

file1.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

void count(int i)
{
    static int num = 0;
    num += i;
    printf("current value of num: %d\n", num);
}

int main()
{
    count(1);
    count(3);
    return 0;
}

Kết quả khi chạy chương trình sẽ là:

file2.sh
1
2
current value of num: 1
current value of num: 4

Biến num khai báo static như trên có 2 đặc điểm:

  1. Do được khai báo static nên chỉ được khởi tạo 1 lần duy nhất và tồn tại suốt thời gian chạy của chương trình. Giá trị của biến count sẽ được tích luỹ mỗi khi hàm count được gọi.
  2. Do khai báo trong nội bộ hàm count nên biến chỉ có thể được nhìn thấy bởi các câu lệnh trong hàm count. Nói cách khác, biến nm là 1 biến nội bộ (local variable).

Tuy vậy bạn sẽ bất ngờ khi bắt gặp những cách sử dung static trong như ví dụ dưới đây:

main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

static int a = 0;

static void count(int i)
{
    static int num = 0;
    num += i;
    printf("current value of num: %d\n", num);
}

int main()
{
    a += 1;
    printf("value of a: %d\n", a);
    count(1);
    count(3);
    return 0;
}

ta bắt gặp static ở 2 nơi nữa:

  1. static trong khai báo hàm
  2. static trong khai báo biến toàn cục

2 từ khoá static này có ngữ nghĩa như thế nào?

Để hiểu được ngữ nghĩa mới của static này, ta cần hiểu 1 khái niệm: đơn vị biên dịch (translation unit).

Mỗi project thường được viết trên nhiều file (vì mục đích phân chia module, đảm bảo tính dễ bảo trì). Mỗi file.c trong dự án sẽ là 1 đơn vị biên dịch. Quá trình biên dịch 1 project C sẽ là: biên dịch các đơn vị độc lập .c ra các object file .o (*.obj) và liên kết (link) các đơn vị object file thành chương trình.

Mỗi đơn vị sẽ có các thủ tục (procedure) hoặc function riêng. Code ở 1 đơn vị biên dịch có thể sử dụng thủ tục hoặc hàm, hay cả biến toàn cục ở đơn vị biên dịch khác. Ví dụ:

main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//-----------------------
//A.c

int avar;

void a() {};

void b() {};

// -----------------------
//C.c

extern int avar;

void c() {};

void d() {};

thì trong a() của A.c ta có thể gọi c() một cách thoải mái. Biến avar sẽ được sử dụng cả ở A.c và C.c (biến toàn cục thực thụ!)

Để hạn chế việc sử dụng này (tránh va đụng tên hàm giữa các đơn vị biên dịch), người ta đưa khái niệm hàm tĩnh (static function) và biến tĩnh (static global variable).

Ngữ nghĩa:

  • Biến toàn cục tĩnh sẽ có phạm vi trên đơn vị biên dịch. Điều đó có nghĩa là đơn vị khác không có cách nào truy cập được biến này.
  • Hàm tĩnh sẽ có phạm vi trên đơn vị biên dịch. Điều đó có nghĩa đơn vị khác không thể truy cập được hàm này.

Do đó

main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//-----------------
//A.c

static int avar;

static void a() {};

void b() {};

//------------------
//C.c

extern int avar;

void c() {};

void d() {};

Nếu ta khai báo static như trên, các hàm c, d trong C.c sẽ không thể nào truy cập được hàm a cũng như biến avar (dù rằng avar được khai báo extern trong C.c).

Tóm lược

static có 2 ngữ nghĩa:

  • Khi được sử dụng trong phạm vi toàn cục của 1 đơn vị biên dịch, static hạn chế truy cập từ các đơn vị biên dịch khác (áp dụng với cả hàm và biến toàn cục).
  • Khi được sự dụng trong phạm vi cục bộ của 1 thủ tục hay hàm, static có nghĩa là biến được khai báo tồn tại trong suốt thời gian chạy của chương trình và chỉ được khởi tạo 1 lần duy nhất.
Copyright © 2015 kỹ thuật máy tính