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

Unittest là gì

Unit tes là các test dùng để test kiến trúc nội tại của chương trình, unit test gắn liền với thiết kế chương trình. Khi viết unit test, tôi thường kiểm tra xem các hàm có được gọi và gọi đúng với parameter cần thiết hay không. Mỗi một unit test chỉ nên test 1 thứ.

Đặc điểm của unit test là rất ngắn, một test case chỉ nên được viết dưới 10 dòng. Nếu bạn cần viết hơn, hãy suy nghĩ lại về thiết kế của mình. Các developer nên viết unit test cho các phần code mình viết. Tôi thường setup môi trường phát triển, để bất cứ khi nào bạn commit một đoạn code, chương trình quản lý mã nguồn sẽ chạy test tự động liên quan đến đoạn code đó. Điều này giúp tôi kiểm tra ngay được code mình viết có gây ảnh hưởng tới các phần khác hay không.

Chính vì thế, unit test cần được chạy rất nhanh. Mỗi một đoạn code chỉ nên được test một lần. Nếu bạn có 2 method A và B, B gọi đến A, code A đã được viết test, thì code test cho B không nên test lại A lần nữa

Unit test không nhất thiết phải cover hết code của bạn. Nếu cover được đầy đủ thì rất tốt, nhưng công sức bỏ ra sẽ rất lớn. Hãy viết unit test đủ để bạn thấy tự tin khi deploy code của mình.

Sử dụng mock với unittest

Tôi có một class sinh ra empty image với kích thước có sẵn, kèm theo barcode image ở vị trí đã được định trước

generator/images.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
26
27
28
29
30
class BackCoverImage(object):

    BACK_COVER_IMAGE_PATH = 'assets/images/empty-barcode-image.jpg'
    BARCODE_IMAGE_SIZE = (650, 195)
    BARCODE_IMAGE_POSITION = (1925, 2300)

    def __init__(self, barcode):
        self.barcode = barcode.upper().replace('_', '-')

    @lazy
    def barcode_image(self):
        params = (
            ('cpaint_function', 'BuildBarcode'),
            ('cpaint_argument[]', self.barcode),
            ('cpaint_argument[]', 0),
            ('cpaint_argument[]', 5),
            ('cpaint_response_type', 'TEXT')
        )

        BARCODE_GENERATE_SITE = 'http://www.barcoding.com'
        BARCODE_GENERATE_URL = '%s/upc/buildbarcode.asp' % BARCODE_GENERATE_SITE
        url = BARCODE_GENERATE_URL + "?" + "&".join("%s=%s" % (k, v) for k, v in params)
        res = requests.get(url)
        image = utils.get_image_from_url(BARCODE_GENERATE_SITE + res.content)
        return image.resize(self.BARCODE_IMAGE_SIZE, Image.ANTIALIAS)

    def run(self):
        image = Image.open(self.BACK_COVER_IMAGE_PATH)
        image.paste(self.barcode_image, self.BARCODE_IMAGE_POSITION)
        return image

Để sinh ra barcode, tôi connect tới một webservice và lấy dữ liệu về. Hàm utils.get_image_from_url trả về Image object từ content của một URL.

decorator @lazy biến một method của class thành property của class đó, và cached lại result, do đó nếu bạn gọi tới property lần thứ hai, bạn sẽ sự dụng lại giá trị từ trong cached

Đây là code test cho class trên

test_back_cover_image.py
1
2
3
4
5
6
7
8
9
10
import hashlib
from PIL import Image
from generator import images

class TestBackCoverImage(unittest.TestCase):
    def test_generate_image(self):
        generator = images.BackCoverImage('124124')
        image = generator.run()
        checksum = hashlib.md5(image.tostring()).hexdigest()
        self.assertEquals('efeae3cb498bbd57325991c2ac5346ad', checksum)

Đoạn code trên generate BackCoverImage với một barcode xác định trước, và so sánh check sum của image được sinh ra, với image mà tôi đã sinh ra từ trước

Tuy nhiên, có vấn đề ở đây. Đó là mỗi lần tôi chạy code test, tôi sẽ phải connect tới service của http://www.barcoding.com. Tức là tốc độ của code test sẽ bị ảnh hưởng bởi network, hơn nữa hàm run() của class BackCoverImage gọi tới barcode_image, nếu chúng ta test như trên, thì code test không phải là một unit test, mà là một integration test. Để giải quyết vấn đề này, chúng ta sử dụng thư viện mock

test_back_cover_image.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import mock
from PIL import Image
from generator import images

class PropertyMock(mock.Mock):
    def __get__(self, instance, owner):
        return self()

class TestBackCoverImage(unittest.TestCase):
    def test_generate_image(self):
        mock_barcode = PropertyMock()
        barcode_image = Image.open('StoryTree/assets/images/barcode_image.png')
        mock_barcode.return_value = barcode_image
        with mock.patch.object(images.BackCoverImage, 'barcode_image', mock_barcode):
            generator = images.BackCoverImage('storytree_124124')
            image = generator.run()
            checksum = hashlib.md5(image.tostring()).hexdigest()
            self.assertEquals('efeae3cb498bbd57325991c2ac5346ad', checksum)

Tôi đã mock thuộc tính barcode_image của class BackCoverImage với PropertyMock. Tốc độ của test được cải thiện đáng kể, từ 3-4s khi test không có mock, xuống < 0.3s

Xét tiếp ví dụ tiếp theo, tôi có một class Order, mỗi khi muốn order, tôi cần sinh ra một pdf file cho class Order. Pdf file này cần có một page được sinh ra từ class BackCoverImage

order.py
1
2
3
4
5
6
7
8
9
10
from django.db import models
from generator import images

class Order(models.Model):
    key = models.AutoField(primary_key=True)
    ...

    def create_pdf_file(self):
        back_image = images.BackCoverImage(self.pk).run()
        ...

Để test hàm create_pdf_file, chúng ta sẽ mock BackCoverImage.run với một Image và kiểm tra xem hàm đó có được gọi hay không?

test_order.py
1
2
3
4
5
6
7
8
9
10
11
12
13
import mock
from PIL import Image
from generator import images
from order import Order

class TestBackCoverImage(unittest.TestCase):
    def test_generate_image(self):
        image = Image.open('StoryTree/assets/images/barcode_image.png')
        mock_backcover = mock.Mock(return_value=image)
        with mock.patch.object(images.BackCoverImage, 'run', mock_backcover):
            order = Order(pk=212)
            order.create_pdf_file()
            mock_backcover.assert_called_once_with(212)

Kết luận

Bằng việc có một bộ test để đảm bảo hệ thống đang hoạt động đúng, bạn giúp các lập trình viên khác trong đội của bạn, hay chính bản thân bạn (sau một thời gian) tự tin khi viết thêm/thay đổi code, mà không sợ ảnh hướng tới logic của những chức năng khác. Điều này đặc biệt hữu ích khi bạn muốn refactor code.

Tuy nhiên để làm điều đó, bộ test của bạn cần chạy trong một thời gian ngắn. Nếu bộ test của bọn tốn vài phút mới thực hiện xong, thì thật khó để yêu cầu các developer khác chạy nó mỗi lần họ commit code.

Bằng cách sử dụng mock, bạn có thể isolate các unittest, đảm bảo mỗi một đoạn code chỉ cần test duy nhất một lần, qua đó tăng tốc độ của unittest lên rất nhiều.

Comments

Giới thiệu

MySQL là một trong những hệ thống cơ sở dữ liệu quan hệ phổ biến số một thế giới, được sử dụng bởi hầu hết các website lớn. Do vậy, việc nắm vững MySQL là yêu cầu không thể thiếu đối với một webmaster.

Kiến trúc logic của MySQL nhìn tổng quan có thể được mô tả như hình dưới đây

Ta có thể thấy MySQL có các component cơ bản như ở dưới đây

  • Connection/thread handling
  • Query cache
  • Parser
  • Optimizer
  • Storage engine

Việc nắm rõ từng chức năng và nhiệm vụ của từng thành phần là điều không thể thiếu trong việc sử dụng MySQL một cách hiệu quả. Bài viết sẽ tập trung giới thiệu về thành phần dưới cùng trong mô hình trên: Storage engine (Máy lưu trữ)

Storage Engine (Máy lưu trữ)

Storage Engine thực chất là cách MySQL lưu trữ dữ liệu trên đĩa cứng. MySQL lưu mỗi database như là một thư mục con nằm dưới thư mục data. Khi một table được tạo ra, MySQL sẽ lưu định nghĩa bảng ở file đuôi .frm và tên trùng với tên của bảng được tạo. Việc quản lý định nghĩa bảng là nhiệm vụ của MySQL server, dù rằng mỗi storage engine sẽ lưu trữ và đánh chỉ mục (index) dữ liệu khác nhau.

Ví dụ: mình chỉ định –datadir là /usr/local/mysql/data và định nghĩa bảng users trong database tên là test như sau

create table users (
    id int not null auto_increment, 
    name varchar(30), 
    password varchar(20), 
    primary key(id)
);

thì trong thư mục /usr/local/mysql/data sẽ có thư mục con là test, và dưới test sẽ có các file

-rw-rw----  1 _mysql  wheel   8624  5  7 17:35 users.frm
-rw-rw----  1 _mysql  wheel  98304  5  7 17:35 users.ibd

Để xem loại storage engine của bảng hiện tại, bạn có thể dùng câu lệnh SHOW DATABASE STATUS

mysql> show table status like 'users' \G
*************************** 1. row ***************************
           Name: users
         Engine: InnoDB
        Version: 10
     Row_format: Compact
           Rows: 2
 Avg_row_length: 8192
    Data_length: 16384
Max_data_length: 0
   Index_length: 0
      Data_free: 0
 Auto_increment: 3
    Create_time: 2013-05-07 17:35:09
    Update_time: NULL
     Check_time: NULL
      Collation: latin1_swedish_ci
       Checksum: NULL
 Create_options:
        Comment:
1 row in set (0.01 sec)

Cụ thể trong trường hợp này: storage engine : innodb loại row : compact Số lượng row dữ liệu : 2 giá trị auto increment tiếp theo: 3 …

Tổng quan các engine

1. MyISAM engine

Đặc điểm
  • full-text indexing
  • compression.
  • spatial functions (GIS)
  • Không hỗ trợ transactions
  • Không hỗ trợ row-level lock.
Lưu trữ

MyISAM lưu mỗi bảng dữ liệu trên 2 file: .MYD cho dữ liệu và .MYI cho chỉ mục. Row có 2 loại: dynamic và static (tuỳ thuộc bạn có dữ liệu thay đổi độ dài hay không). Số lượng row tối đa có thể lưu trữ bị giới hạn bởi hệ điều hành, dung lượng đĩa cứng. MyISAM mặc định sử dụng con trỏ độ dài 6 bytes để trỏ tới bản ghi dữ liệu, do vậy giới hạn kích thước dữ liệu xuống 256TB.

Tính năng:
  • MyISAM lock toàn bộ table. User (MySQL server) chiếm shared-lock khi đọc và chiếm exclusive-lock khi ghi. Tuy vậy, việc đọc ghi có thể diễn ra đồng thời!
  • MyISAM có khả năng tự sửa chữa và phục hồi dữ liệu sau khi hệ thống crashed.
  • Dùng command check table / repair table để kiểm tra lỗi và phục hồi sau khi bị lỗi.
  • MyISAM có thể đánh chỉ mục full-text, hỗ trợ tìm kiếm full-text.
  • MyISAM không ghi dữ liệu ngay vào ổ đĩa cứng, mà ghi vào 1 buffer trên memory (và chỉ ghi vào đĩa cứng sau 1 khoảng thời gian), do đó tăng tốc độ ghi. Tuy vậy, sau khi server crash, ta cần phải phục hồi dữ liệu bị hư hỏng bằng myisamchk.
  • MyISAM hỗ trợ nén dữ liệu, hỗ trợ tăng tốc độ đọc dữ liệu. Mặc dù vậy dữ liệu sau khi nén không thể cập nhật được.

2. InnoDB engine

Đặc điểm
  • Là engine phức tạp nhất trong các engine của MySQL
  • Hỗ trợ transactions
  • Hỗ trợ phục hồi, sửa chữa tốt
Lưu trữ

InnoDB lưu dữ liệu trên 1 file (thuật ngữ gọi là tablespace).

Tính năng:
  • InnoDB hỗ trợ MVCC (Multiversion Concurrency Control) để cải thiện việc truy cập đồng thời và hỗ trợ chiến thuật next-key locking.
  • InnoDB được xây dựng dựa trên clustered index, do đó việc tìm kiếm theo primary key có hiệu năng rất cao. InnoDB không hỗ trợ sắp xếp index do vậy việc thay đổi cấu trúc bảng sẽ dẫn tới toàn bộ dữ liệu phải được đánh chỉ mục từ đầu (CHẬM với những bảng lớn).

3. Memory engine

Đặc điểm
  • Còn được gọi là HEAP tables.
Lưu trữ

Tất cả dữ liệu đều nằm trên memory.

Tính năng:
  • Sau khi server restart, cấu trúc bảng được bảo toàn, dữ liệu bị mất hết.
  • Memory engine sử dụng HASH index nên rất nhanh cho query lookup.
  • Memory engine dùng table-level locking do vậy tính concurrency không cao.

4. Archive engine

Đặc điểm
  • Chỉ hỗ trợ Insert và Select.
  • Không đánh chỉ mục
  • Dữ liệu được buffer và nén bằng zlib nên tốn ít I/O, tốc độ ghi do đó cao.
Tính năng:
  • Tốc độ ghi cao, phù hợp cho ứng dụng log.

5. CSV engine

Đặc điểm
  • Coi file CSV như là 1 table.
  • Không hỗ trợ đánh chỉ mục
Tính năng:
  • Nếu bài toán là trích xuất thông tin từ file CSV và ghi vào cơ sở dữ liệu, đồng thời cần kết quả CSV ngay từ DB, engine này có vẻ thích hợp.

6. Falcon engine

Đặc điểm
  • Được thiết kế cho phần cứng hiện đại: server 64 bit, bộ nhớ “thênh thang”
  • Vẫn còn khá mới, chưa có nhiều usercase

7. Maria engine (Cơ sở dữ liệu liên quan: MariaDB)

Đặc điểm
  • Được thiết kế bởi những chiến tướng dày dạn kinh nghiêm của MySQL, với mục đích thay thế MyISAM
  • Hỗ trợ transactions theo lựa chọn
  • Khôi phục lỗi
  • Row-level locking và MVCC
  • Hỗ trợ BLOB tốt hơn.

Tiêu chí lựa chọn engine

  • Transactions: Nếu ứng dụng yêu cầu transactions, InnoDB là lựa chọn duy nhất. Nếu không yêu cầu transactions, MyISAM là lựa chọn tốt.
  • Concurrency: Nếu yêu cầu chịu tải cao và không cần thiết transactions, MyISAM là lựa chọn số 1.
  • Sao lưu: Các engine đều phần nào hỗ trợ sao lưu. Ngoài ra ta cần hỗ trợ sao lưu trên cả quan điểm thiết kế hệ thống. Ví dụ: bạn thiết kế database server gồm master và slave, master yêu cần transaction nên dùng innodb, slave cần sao lưu và đọc nên có thể dùng MyISAM. Cơ chế đồng bộ master-slave sẽ giúp bạn quản lý sự khác nhau giữa các engine nhưng đảm bảo tính sao lưu. Tiêu chí này có trọng số nhỏ.
  • Phục hồi sau crash: MyISAM có khả năng phục hồi sau crash kém hơn InnoDB.
  • Tính năng theo yêu cầu hệ thống: Nếu yêu cầu là logging, MyISAM hoặc Archive là lựa chọn hợp lý. Nếu cần lưu trực tiếp CSV, CSV engine là lựa chọn đáng cân nhắc. Nếu ứng dụng không thay đổi dữ liệu mấy (ví dụ cơ sở dữ liệu sách), MyISAM và tính năng nén là lựa chọn phù hợp.

Kết luận

Bài viết này đã giới thiệu tổng quan về storage engine, một thành phần quan trọng của hệ thống cơ sở dữ liệu. Một số engine tiêu biểu và tính năng đặc điểm cũng được đưa ra. Tiêu chí chọn lựa mỗi loại engine cũng được giới thiệu.

Hy vọng qua bài viết, bạn có cái nhìn tổng quan về database storate engine nói chung, và MySQL storage engine nói riêng, đồng thời hiểu được tầm quan trọng của việc chọn lựa storage engine.

Tham khảo

  1. High performance MySQL, 2ed
  2. Storage Engines
  3. Storage Engine Introduction
Comments

What is memoization

Trước hết chúng ta làm quen với khái niệm memoization. Ngôn ngữ ở đây là Python, bài toán là viết hàm tính giai thừa (n!)

Hàm giai thừa thông thường sẽ được viết đệ quy như sau:

python.py
1
2
3
def fac(n):
    if n < 2: return 1
    return n * fac(n - 1)

Có gì không ổn ở đoạn code này ? Cách giải quyết hoàn toàn không có vấn đề, nhưng nếu tinh ý bạn sẽ nhận thấy có 1 khối lượng tính toán bị lặp lại khá nhiều khi chạy nhiều hàm fac(n). VD, nếu tính fac(3), fac(4) và fac(10) lần lượt sẽ đòi hỏi 3 flow tính toán riêng rẽ mà không có reuse: fac(3) sẽ tính đệ quy từ fac(2) xuống fac(1), fac(4) tính đệ quy từ fac(3) xuống fac(1) và fac(10) tính đệ quy từ fac(9) xuống fac(1) !

Áp dụng memoization dưới dạng dict, ta có thể viết hàm fac_m như sau:

python.py
1
2
3
4
5
6
memo = {}
def fac_m(n):
    if n<2: return 1
    if n not in memo:
        memo[n] = n * fac_m(n-1)
    return memo[n]

Ở đây memo đóng vài trò như 1 cache. fac(3) sẽ generate ra 3 record in cache, và fac(4) sẽ hit cache khi chạy đệ quy được 1 lần. Tương tự fac(10) sẽ hit cache khi đệ quy xuống đến fac(4)

Như vậy memoization đơn giản chỉ là tìm cách nhớ những phần tử để giảm khối lượng tính toán

Memoization có thể implement dưới dạng function…

python.py
1
2
3
4
5
6
7
def memoize(fn, arg):
    memo = {}
    if arg not in memo:
        memo[arg] = fn(arg)
    return memo[arg]
def fac_m_f(n):
    return memoize(fac,n)

…hoặc class

python.py
1
2
3
4
5
6
7
8
9
class Memoize:
    def __init__(self, f):
        self.f = f
        self.memo = {}
    def __call__(self, *args):
        if not args in self.memo:
            self.memo[args] = self.f(*args)
        return self.memo[args]
fac= Memoize(fac)

Thêm 1 step nữa, thay vì “fac=Memoize(fac)” như ở trên, bạn có thể viết hàm mới theo kiểu decorator

python.py
1
2
3
4
@Memoize
def fac_m_d(n):
    if n<2: return 1
    return n * fac_m_d(n-1)

Decorator ở đây là từ khoá “@Memoize” trước định nghĩa của hàm fac_m_d

Vậy decorator trong Python là gì và cách dùng ra sao ?

Python decorator

Trong số các design pattern, có 1 design pattern gọi là “decorator design pattern”. Python decorator chỉ là 1 cách implement của decorator design pattern. 2 khái niệm này không hoàn toàn giống nhau. Một điểm nữa cần nhớ là, memoization ở trên chỉ là 1 trong các ứng dụng của python decorator, python decorator còn có nhiều ứng dựng khác.

Mọi function trong python đều là object, cho phép ta có thể assign funtion cho variable hoặc defince function trong chính 1 function khác. Dựa vào đó, decorator có thể dưới dạng decorator function như ví dụ dưới đây:

python.py
1
2
3
4
5
6
7
8
9
10
def gotham(f):
    def inside_gotham():
        print "Gotham needs Batman"
        f()
    return inside_gotham

@gotham
def batman():
    print "Batman Here! Gotham is saved! "
batman()

Đoạn code sẽ cho output:

1
2
Gotham needs Batman
Batman Here! Gotham is saved!

Cơ chế của decorator có thể hiểu đơn giản là, khi intepreter đọc đến đoạn code đefine function với decorator, interpreter sẽ evaluate function 1 cách bình thường và pass function object kết quả thẳng cho decorator(dưới dạng function hoặc class). Decorator(function hoăc class) lấy agrument là 1 function object và return kết quả là 1 function object khác.

Function object kết quả nói trên gồm function object ban đầu đã được gói lại và “thêm thắt”, và từ nay về sau sẽ được gọi thay cho function object ban đầu mỗi khi có lệnh call.

Ngoài memoization bên trên, bạn có thể dễ thấy rất nhiều ứng dụng của decorator trong các task liên quan đến wrap VD như:

Timing, benchmark tính toán thời gian run code

python.py
1
2
3
4
5
6
7
8
9
10
11
12
def time_cal(func):
    def wrapper(*arg):
        t = time.time()
        res = func(*arg)
        print func.func_name, time.time()-t
        return res
    return wrapper

@time_cal
def fac(n):
    if n < 2: return 1
    return n * fac(n - 1)

hay trong web application, nếu bạn đã dùng Flaskr, bạn có thể thấy đoạn code sau

python.py
1
2
3
4
@mod.route('/me/')
@requires_login
def home():
...

Ở đây trang web của bạn ở sublink “…/me” sẽ được đảm bảo chỉ viewable với user đã login. Decorator “@requires_login” có thể viết ở 1 file độc lập và mọi hàm cần tính đảm bảo như trên chỉ cần thêm “@requires_login” đằng trước.

python.py
1
2
3
4
5
6
7
8
9
10
from functools import wraps
...
def requires_login(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if g.user is None:
            flash(u'You need to be signed in for this page.')
            return redirect(url_for('users.login', next=request.path))
        return f(*args, **kwargs)
    return decorated_function

Kết luận

  • Memoization: pattern dùng để nhớ các tính toán nhằm làm giảm workload khi gặp các bài toán đệ quy
  • Decorator pattern: decorator design pattern
  • Python Decorator: Python tools để implement decorator pattern
Copyright © 2015 kỹ thuật máy tính