LoginSignup
2
2

More than 5 years have passed since last update.

Các bài học lập trình ứng dụng RESTful ~ Chuyên đề phát triển Web - [Chương 10 ] Refactoring ( phía Server)

Last updated at Posted at 2015-11-24

:large_blue_circle: Xin chào các bạn

bài viết này được dịch từ リファクタリング(サーバー編)- AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第10回】マニュアル của tác giả @k_shimoji

Hôm nay chúng ta sẽ học về các cấu trúc lại code hay còn gọi là "refactoring".

:large_blue_circle: Refactoring là gì ?

Refactoring – tái cấu trúc – là quá trình làm thay đổi mã hiện có bên trong mà không thay đổi hành vi bên ngoài của nó. Nói cách khác, tức là thay đổi cách nó thực hiện, nhưng không không thay đổi nó làm cái gì. Mục đích là để cải thiện cơ cấu nội bộ.

:large_blue_circle: Các bước Refactoring

  1. Xác định nơi cần refactoring.
  2. Viết các test để kiểm tra .
  3. refactoring
  4. test
  5. Trở lại bước 1

:one: Xác định nơi cần refactoring

Code smells (code mà bốc mùi, hoặc có mùi lạ trong code) là bất kỳ triệu chứng bất ổn nào bên trong mã nguồn của một chương trình, mà vì nó có thể sẽ dẫn đến các vấn đề lớn hơn. Code smells không phải là bugs (lỗi lập trình), nghĩa là chúng không làm sai chứ năng của ứng dụng. Thay vào đó, chúng là biểu hiện của sự yếu kém trong thiết kế và sẽ làm cho quá trình phát triển ứng dụng bị chậm lại hoặc tăng nguy cơ của bugs hoặc lỗi trong tương lai.
* Quote: Định nghĩa các code bốc mùi *

  • Lặp code
  • method quá dài
  • Class quá to
  • Quá nhiều arguments
  • Quá nhiều sự thay đổi
  • Biến thay dổi
  • Các thuộc tính, sự phụ thuộc bất hợp lý của operation
  • Header của dữ liệu
  • Quá tuân thủ các kiểu dữ liệu nguyên thủy.
  • Lệnh Switch
  • Thừa kế song song
  • Class lười biếng
  • Những thành phần được sinh ra đáng nghi
  • Thuộc tính tạm thời
  • Chuỗi tin nhắn
  • Trung gian
  • Các mối quan hệ không đúng
  • Class interface không phù hợp
  • Thư viện quá cũ
  • Data class
  • Từ chối thừa kế
  • Comment

:point_up: Mình sẽ phân loại các mùi này ra

Việc phân nhóm này hoàn toàn là một ý tưởng cá nhân nhằm mục đích tăng tính dễ nhớ. Đây không phải là thuật ngữ hay keyword nên chỉ cần hiểu được chúng thì bạn muốn gọi thế nào cũng được. Chúng ta sẽ đi vào từng nhóm: :smirk:

  • Mùi quá rõ ràng
  • Mùi xuất hiện khi bạn bắt đầu đọc code
  • Cấu trúc rắc rối
  • Hệ thống lỗi thời
  • Mùi tanh
  • Mùi mồ hôi
  • Mùi lừa dối

Tôi sẽ giải thích ở phần dưới.
Cho mỗi mùi, tôi sẽ viết một "cách khử mùi" và "những nguyên tắc nó vi phạm", còn phần giải thích thì ở bài viết này chắc là không đủ, vì vậy mong các bạn tìm kiếm thêm thông tin về nó trên google nếu các bạn thực sự hứng thú.

Mùi quá rõ ràng

Không cần làm gì cũng biết code có mùi, nhìn qua chứ không cần đọc code luôn.

  • 2. Method quá dài
    • Hãy tách nó ra.
  • 3. Class quá lớn
    • Tách nó ra.
  • 4. Quá nhiều arguments
    • Hãy phân loại các object rõ ràng hơn.
  • 10. Quá nhiều lệnh switch
    • Bạn có một Switch Statements quá phức tạp hoặc một chuỗi các vòng if then lồng nhau rất rắc rối. Hãy sử dụng hướng đối tượng.

Mùi xuất hiện khi bạn bắt đầu đọc code

Những mùi này ngay lập tức xuất hiện khi bạn đọc qua các tiêu đề

  • 1. Lặp code
    • Vi phạm nguyên tắc DRY, hãy hạn chế việc copy paste lại code.
  • 12. Class lười biếng
    • Một class nhỏ tồn tại mà không có nhiều ý nghĩa với những gì bạn đang làm, tốt nhất là tạo một role ở class khác chứ không cần để riêng ra.
  • 16. Trung gian
    • Việc hiểu và thay đổi một class thường sẽ tốn rất nhiều chi phí và thời gian. Vậy nên nấu như có một class nào đó không đáng để bỏ ra những chi phí như vậy thì hãy xóa nó đi.
  • 18. Interface Class không phù hợp
    • Ai đó đã làm một việc tương tự ? hãy ngầm hiểu với nhau.
  • 20. Data class
    • Hay còn gọi là "Thiếu domain model". Hãy nhớ data và các hành vi.

Cấu trúc rắc rối

  • 5. Quá nhiều sự thay đổi
    • Vi phạm nguyên tắc SRP (single responsibility principle). Một class đã làm việc quá tải, hay chia sẻ công việc của nó.
  • 6. Biến bị thay đổi
    • Chia sẻ 1 công việc cho quá nhiều class. Hãy phân chia trách nhiệm rõ ràng hơn .
  • 11. Thừa kế song song
    • Một ví dụ điểm hình của vi phạm nguyên tắc OCP (open / closed principle). Mỗi khi bạn khởi tạo một class con của một class thì bạn thấy rằng mình cần phải tạo tiếp một class con của một class khác tương ứng. Hãy chia sẻ đúng công việc của class cha và class con.

Hệ thống lỗi thời

Ở thời điểm trước ( phụ thuộc vào hệt thống thời đó hoặc thời gian phát triển mà nó có khi chỉ là sự thay đổi rất nhỏ) nó có thể không phải mùi, tuy nhiên nó khiến cho việc bảo trì trở nên khó khăn khi công nghệ đó đã quán cũ

  • 7. Thuộc tính, các sự gắn kết không hợp lệ của các hoạt động
    • Các luật Roles của class trở nên không rõ ràng, hãy sửa roles cho thích hợp
  • 15. Chuối tên gọi quá dài
    • Trong code của bạn xuất hiện một chuỗi như sau: methodA().methodB().methodC().methodD(); Hãy tổ chức lại chúng vì rất phức tạp.
  • 17. Các mối quan hệ không đúng cách
    • Khi một class sử dụng đến một thuộc tính hoặc phương thức của một class khác mà đáng ra phương thức và thuộc tính đó nên là private.
  • 21. Từ chối thừa kế
    • Nếu một class thực thi chỉ một hoạt động mà phải kéo theo thêm một class khác thì tại sao class kéo theo này nên tồn tại?

Mùi cá

Mình tạm gọi vui cái mùi này giống như mùi cá tanh.
Nó thiếu data modeling, thiếu sự cân nhắc trước khi đóng gói. Hãy cẩn thận hơn dù quá một chút thời gian.

  • 8. Data gửi vào header
    • Data cần luôn được sử sử dụng ở cùng một set, hãy cố gắng để nó thật sát với object.
  • 9. Tuân thủ các kiểu dữ liệu cơ bản
    • Hãy tạo một ValueObject
  • 14. Thuộc tính tạm thời
    • Các biến (field) không được sử dụng hoặc đã sử dụng xong. Hãy chuyển nó thành các biến ngoài hoặc NullObject

Mùi mồ hôi

Bạn đã làm việc rất vất vả, tuy nhiên code của bạn vẫn rất tồi, và thứ duy nhất có thể xuất ra là mồ hôi.

  • 13. Nghi ngờ quá nhiều
    • Bạn đã code mà nghĩ quá nhiều đến những thứ tương lai cần dùng đến, vì vậy bạn đã implement rất nhiều thứ không cần thiết tại thời điểm hiện tại. Hãy chỉ implement nếu bạn thực sự cần nó, chừng nào bạn vẫn chưa cần nó, hãy cố gắng để chương trình của bạn đơn giản nhất có thể.

Mùi lừa dối

Mặc dù code không cố tình viết sai, nhưng kết quả nó lại sai.

  • 19. Không sử dụng được class thư viện
    • Thư viện không thể sử dụng như chính khả năng của nó, hãy sửa lại để có thể sử dụng được, đừng phó mặc tất cả cho thư viện.
  • 22. Comments
    • Comments quá cũ hoặc sai sự thật. Hãy cố đọc hiểu chương trình càng nhiều càng tốt mà không chú ý đến comment. Nó có thể không chú thích gì cho code cả ( tại sao lại dùng logic này , vv...) chỉ với mục tiêu là bạn muốn comment, hãy cố gắng viết code thật dễ hiểu mà không cần tóm tắt nó bằng comment.

:two: Bạn đã có test nào chưa? Nếu chưa, hãy viết test ngay - PhpUnit

Refactoring is nghĩa là "không thay đổi hoạt động của chương trình" nhưng "cải thiện được cấu trúc bên trong".
Để chắc chắn rằng những hành vi của chương trình không thay đổi. bạn không thể chỉ test một lần

Hãy bắt đầu test nào.

Chú ý rằng với việc viết test thì có một điều rất quan trọng là "Test phải dễ dàng để viết".

:three: Refactoring

Có một vài kỹ thuật cho mỗi "code smell" ( mùi của code ), tuy nhiên tôi không thể mô tả hết được, ở workshop này mình chỉ giữ mọi thứ thật đơn giản.

Bởi vì hàn Upload ở bài trước có mùi của "method quá dài", hãy thử refactor nó bằng cách áp dụng cách "Chia Method".

:four: Test

Hãy kiểm tra kết quả bằng cách test sau khi đã hoàn tất refactoring.
Để viết một test code tốt,
Điều quan trọng là những gì bạn muốn test phải được phân biệt rõ ràng Nếu bạn không muốn test tất cả mọi thứ.

Các test phải được implement như là một "giá trị giả", mình sẽ tiếp tục test hiệu quả chương trình.

:five: Trở lại bước 1

Refactoring cần luôn được thực thi.
Hãy tiếp tục làm giảm các gánh nặng kỹ thuật cho chương trình.

:large_blue_circle: Mục lục bài học

  • Chuẩn bị
    • Duy trì Branch
    • Cài đặt Composer
    • Cài PhpUnit
    • Tạo test cho DB
  • Bài 1 Thiết lập PHPUnit và tạo một test đơn giản.
  • Bài 2 test hàm upload
  • Bài 3 refactoring hàm upload

:large_blue_circle: Chuẩn bị

Phần chuẩn bị sẽ giống nhau ở mỗi bài, vì vậy tôi đã tổng hợp nó ở một entry riêng. Các bạn vui lòng xem ở link này All 12 times of study sessions in do I have use of Git - RESTful application workshops will be built on AWS ~ Web development workshop ~ - Qiita để chuẩn bị .

  • Git: Bây giờ mình sẽ tạo branch vol/09 và làm việc trên nó.

OK, bước đầu đã xong

:warning: *** Tôi xin nhắc lại ở bài 5 và bài 6, chúng ta đã có những table bị thay đổi. ***

  • Bài 5
    • Tạo Table dành cho việc đăng ký thành viên ( Thực hiện cho chức năng login )
  • Bài 6
    • Thêm một cột cho table TODO list ( Thêm cột Owner và cột assignee cho chức năng phân công công việc)

Các bạn nên tham khảo lại các link sau để rõ hơn.
Tạo Table dành cho việc đăng ký thành viên
Thêm một cột cho table TODO list

Từ giờ trở đi, mình sẽ sử dụng phpUnit để tạo test code. Bởi vì nó được tạo bằng composer, vì vậy mình cũng cần cài sẵn composer nữa.

Thiết lập Composer, cài đặt phpUnit

Composer là một công cụ quản lý các gói tin được sử dụng trong php. Bây giờ mình sẽ cài đặt phpUnit bằng cách sử dụng Composer.

Download Composer

Công việc đầu tiên tất nhiên là download Composer.
Vào thư mục /var/www/study/rest-study và sử dụng lênh wget để download.

DownloadComposer
cd /var/www/study/rest-study
wget http://getcomposer.org/composer.phar

Sửa composer.json

Tiếp theo, mình cần sửa file composer.json ( file định nghĩa những thứ bạn muốn quản lý bằng Composer).
Mình đã xóa debug_kit vì nó không thực sự cần thiết.
:warning: Tôi sẽ không đi sâu vào cấu trúc Composer và composer.json format ở bài này, rất xin lỗi.

/var/www/study/rest-study/composer.json
 {
    "name": "cakephp/cakephp",
    "description": "The CakePHP framework",
    "type": "library",
    "keywords": ["framework"],
    "homepage": "http://cakephp.org",
    "license": "MIT",
    "authors": [
        {
            "name": "CakePHP Community",
            "homepage": "https://github.com/cakephp/cakephp/graphs/contributors"
        }
    ],
    "support": {
        "issues": "https://github.com/cakephp/cakephp/issues",
        "forum": "http://stackoverflow.com/tags/cakephp",
        "irc": "irc://irc.freenode.org/cakephp",
        "source": "https://github.com/cakephp/cakephp"
    },
    "require": {
        "php": ">=5.2.8",
        "ext-mcrypt": "*"
    },
    "require-dev": {
-       "phpunit/phpunit": "3.7.*",
-       "cakephp/debug_kit" : "2.2.*"
+       "phpunit/phpunit": "3.7.*"
    },
    "bin": [
        "lib/Cake/Console/cake"
    ]
 }

Cài đặt phpUnit bằng Composer

Mình chạy lệnh sau.

php composer.phar install

Tạo test cho DB

Để an toàn cho study database, hãy tạo một database tên study_test.
Tôi sử dụng phpMyAdmin.

Đầu tiên bạn cần đăng nhập vào PHPMyAdmin
URL là http://(PublicIP)/phpMyAdmin/.
Nếu bạn chưa thay đổi gì thì ID và password sẽ đều là study.
Sau các bước trên, bạn cần copy study database và tạo một database mới tên là study_test.

  1. Sau khi đăng nhập, ở nơi bạn chọn study database, click vào phần "operation" như trong ảnh sau.

phpMyAdmin_1.png

  1. Tiếp theo, phần "Copy database to:" mình điề tên là "study_test", Tick vào phần "Structure only", sau đó chạy.
    phpMyAdmin_2.png

  2. Nếu kết quả như dưới đây là bạn đã thành công
    phpMyAdmin_3.png

Các bước chuẩn bị đã xong, bắt đầu bài 1 nào

:large_blue_circle: Bài 1: Tạo một test đơn giản bằng PhpUnit

Ở bài 3 mình sẽ refactoring hàm upload mà mình đã tạo ở bài trước. Tuy nhiên trước tiên mình cần thử tạo một test code bằng phpUnit để kiểm tra bằng bài hàm đơn giản.

  • Lấy toàn bộ TODO List(TodolistsController::index())
  • Lấy TODO 1(TodoListsController::view())

Tôi sẽ thử viết test code cho 2 method này.

Tương ứng thì để thêm dữ liệu vào database được giới thiệu bằng Json dưới đây, hãy chắc chắn là nó có thể tổng hợp được chúng.

  • TodolistsController::index()
TODOlist
[
    {
        "TodoList": {
            "id": "1000",
            "todo": "eat lunch",
            "status": "1",
            "owned": true,
            "assigned": true
        },
        "Owner": {
            "id": "1000",
            "name": "takumi"
        },
        "Assignee": {
            "id": "1000",
            "name": "takumi"
        }
    }
]
  • TodolistsController:: view()
TODO1件

{
    "TodoList": {
        "id": "1000",
        "todo": "eat lunch",
        "status": "1",
    },
    "Owner": {
        "id": "1000",
        "name": "takumi"
    },
    "Assignee": {
        "id": "1000",
        "name": "takumi"
    }
}

Chi tiết các file cần chỉnh sửa

Thao tác file Mô tả
Sửa app/Config/core.php Thêm thiết lập cho phpUnit
Sửa app/Config/database.php set database dùng để test
Thêm mới app/Test/Fixture/TodoListFixture.php test quy trình gửi dữ liệu TODO vào DB
Thêm mới app/Test/Fixture/UserFixture.php test quy trình gửi dữ liệu User vào DB
Thêm mới app/Test/Case/Controller/TodoListsControllerTest.php Nội dung chính của test code

app/Config/core.php

Mỗi class của phpUnit mình sẽ thêm cài đặt để nó tự động được load.

app/Config/core.php

〜略〜

 /**
  * Configure the cache for model and datasource caches. This cache configuration
  * is used to store schema descriptions, and table listings in connections.
  */
 Cache::config('_cake_model_', array(
    'engine' => $engine,
    'prefix' => $prefix . 'cake_model_',
    'path' => CACHE . 'models' . DS,
    'serialize' => ($engine === 'File'),
    'duration' => $duration
 ));
+
+require_once ROOT . DS . 'vendor' . DS . 'autoload.php'; 

  • Trong autoload.php, vì nó chứa các thiết đặt để tự động đọc class mà bạn cài đặt trong Composer, Các định Path cần load.

app/Config/database.php

Thêm các thông tin để kết nối đến database mà bạn vừa tạo.

app/Config/database.php

〜略〜

 class DATABASE_CONFIG {

    public $default = array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'study',
        'password' => 'study',
        'database' => 'study',
        'prefix' => '',
        'encoding' => 'utf8',
    );

    public $test = array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => 'localhost',
-       'login' => 'user',
-       'password' => 'password',
-       'database' => 'test_database_name',
+       'login' => 'study',
+       'password' => 'study',
+       'database' => 'study_test',
        'prefix' => '',
-       //'encoding' => 'utf8',
+       'encoding' => 'utf8',
    );
 }

app/Test/Fixture/TodoListFixture.php

"Fixture (fixtures)" là class để thêm một dữ liệu của Todo vào test database .

TodoListFixture.php
<?php
class TodoListFixture extends CakeTestFixture {
    public $import = 'TodoList';
    public $records = array (
        array (
            "id" => 1000,
            "todo" => "eat lunch",
            "status" => "1",
            "owner" => 1000,
            "assignee" => 1000
        )
    );
}
  • public $import = 'TodoList'; giúp mình đọc danh sách trên table TodoList trong database được khai báo ở $default của database.php đó là database mình copy của (study).
  • Data được tạo ở public $records = array ( ... ).

app/Test/Fixture/UserFixture.php

Để gửi dữ liệu của User vào test database mình dùng "Fixture (fixture)" class.

UserFixture.php
<?php
App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');
class UserFixture extends CakeTestFixture {
    public $import = 'User';
    public $records;
    public function init() {
        $this->records = array (
            array (
                "id" => 1000,
                "username" => "yamada",
                "name" => "takumi",
                "password" => (new BlowfishPasswordHasher())->hash("yamada")
            )
        );
        parent::init();
    }
}

Giống như với TodoListFixture.php , set $import$records, tuy nhiên đừng quên mình cũng cần hash pasword nữa, quy trình của chúng chúng ta là implement cài đặt của $records ở hàm init .

app/Test/Case/Controller/TodoListsControllerTest.php

Đây là nội dung chính của test code

app/Test/Case/Controller/TodoListsControllerTest.php
<?php
App::uses('AppController', 'Controller');
class TodoListsControllerTest extends ControllerTestCase {
    public $fixtures = array (
        'app.todo_list',
        'app.user'
    );
    /**
     * 準備
     * @return Controller
     */
    public function setUp() {
        parent::setUp();
        $mocks = array (
            'components' => array (
                'Auth' => array (
                    'user'
                )
            )
        );
        //TodoListsControllerを生成
        $controller = $this->generate('TodoLists', $mocks);
        //Authコンポーネントのuserメソッドをスタブにする
        $loginUser = array (
            "id" => "1000",
            "username" => "yamada",
            "name" => "yamada"
        );
        $controller->Auth->staticExpects($this->any())
            ->method('user')
            ->will($this->returnValue($loginUser));
        $this->controller = $controller;
    }
    /**
     * index関数のテスト
     */
    public function testIndex()
    {
        $this->testAction('/todo_lists.json', array(
            'method' => 'get'
        ));
        $result = $this->vars['res'];
        $expected = array(
            array(
                "TodoList" => array(
                    "id" => "1000",
                    "todo" => "eat lunch",
                    "status" => "1",
                    "owned" => true,
                    "assigned" => true
                ),
                "Owner" => array(
                    "id" => "1000",
                    "name" => "takumi"
                ),
                "Assignee" => array(
                    "id" => "1000",
                    "name" => "takumi"
                )
            )
        );
        $this->assertEquals($expected, $result);
    }
    /**
     * view関数のテスト
     */
       public function testView()
    {
        $this->testAction('/todo_lists/1000.json', array(
            'method' => 'get'
        ));
        $result = $this->vars['res'];
        $expected = array(
            "TodoList" => array(
                "id" => "1000",
                "todo" => "eat lunch",
                "status" => "1"
            ),
            "Owner" => array(
                "id" => "1000",
                "name" => "takumi"
            ),
            "Assignee" => array(
                "id" => "1000",
                "name" => "takumi"
            )
        );
        $this->assertEquals($expected, $result);
    }

}

Mình sẽ kế thừa từ class ControllerTestCase, đây là một class dùng để test TodoListsController. Rộng hơn thì mình sẽ mô tả sau vì nó sẽ chia ra như sau.
- Biến $fixtures
- Bạn sẽ set fixture, nó sẽ gửi data lên trong quá trình chạy test.
- Hàm setUp
- Mình sẽ test TodoListsController, nó sẽ sinh các ví dụ giả để chạy phần này.
- Chạy hàm generate. Tên của Controller để tạo (TodoLists), và truyền thông tin về các đối tương giả định (mock).
- Tại đây hàm user ( hàm sẽ trả về thông tin của login user) của thành phần Auth cho mock, nó sẽ phải chạy hàm và return những thông tin bạn đã set trong $loginUser. Bây giờ chúng ta phải giả lập phần login.
- test code của hàm index
- Với việc xác định url và method ở testAction, bạn có thể giả lập truy cập từ client.
- Giá trị trả về là một chuỗi Json, sẽ được xác nhận bằng hàm assertEquals xem có hợp với chỉ 1 TODO data đã được gửi lên ở fixture hay không.
- test code cho hàm View
- Ngoài ra chúng ta cũng chạy test ở hàm testAction .

Chạy Test

OK, chúng ta đã sẵn sàng để test code! Tôi sẽ giải thích cách thực hiện !

Các bạn cũng có thể chạy command line từ trình duyệt.

Chạy từ trình duyệt

Truy cập URL sau từ trình đuyệt

http://(PublicIP)/rest-study/test.php

Menu sau sẽ xuất hiện trên màn hình ! Hãy click vào vì test class mà mình vừa tạo đã hiển thị.

phpUnit_1.png

Test đã được chạt. Thanh màu xanh hiển thị như sau nghĩa là thành công.
phpUnit_2.png

Để thử, tôi sẽ thử thay đổi data để set vào fixture.
Todo đã được gửi vào fixture, khi bạn chạy nó và sửa nó từ "buy milk" thành "walk the dog" ,

phpUnit_3.png

Chạy từ command line

Vào thư mục /var/www/study/rest-study/app/Console, và chạy với ./cake test app Controller/TodoListsController.

RunByCommandLine
cd /var/www/study/rest-study/app/Console
./cake test app Controller/TodoListsController
  • Trường hợp test thành công

phpUnit_4.png

  • Test thất bại

phpUnit_5.jpg

Tổng kết bài học

Chúng ta đã thực hiện các thao tác như sau

  • :white_check_mark: Tạo mới file app/Config/core.php với nội dung như trên.
  • :white_check_mark: Sửa file app/Config/database.php như hướng dẫn trên.
  • :white_check_mark: Tạo mới file app/Test/Fixture/TodoListFixture.php với nội dung như trên.
  • :white_check_mark: Tạo mới file app/Test/Fixture/UserFixture.php với nội dung như trên.
  • :white_check_mark: Tạo mới file app/Test/Case/Controller/TodoListsControllerTest.php với nội dung như trên.
  • :white_check_mark: Kiểm tra kết quả.
  • :white_check_mark: Commit lên Git

:warning: Hiển thị dụng Diff trên GitHub

第10回 Lesson1 phpUnitの設定と簡単なテスト作成 · suzukishouten-study/rest-study@a17ff3a

Chuyển qua bài 2 nào cả nhà.

:large_blue_circle: Bài 2: Test hàm Upload

Ở bài 2 này, chúng ta sẽ tạo một test của hàm upload để refactor.
Thật ra thì có một bug ở hàm upload của chương trình này, chúng ta sẽ fix nó.

Các file cần chỉnh sửa

Thao tác file Mô tả
Sửa app/Test/Case/Controller/TodoListsControllerTest.php Thêm test cho phần upload
Sửa app/Model/TodoList.php Sửa thứ tự để vào test trong DB
Sửa app/Controller/TodoListsController.php Fix các bug và một vài thứ khác nữa

app/Test/Case/Controller/TodoListsControllerTest.php

Tôi sẽ thêm hàm test cho phần upoad

app/Test/Case/Controller/TodoListsControllerTest.php

〜略〜
    public function setUp() {
        parent::setUp();
        $mocks = array (
+           'methods' => array (
+               'getUploadFileParams'
+           ),
            'components' => array (
                'Auth' => array (
                    'user'
 @@ -90,4 +93,88 @@ public function testView() {
        $this->assertEquals($expected, $result);
    }

+   /**
+    * Test ham upload
+    * Upload 1 file moi lan, gui TODO2 vao DB nhu binh thuong
+    */
+   public function testUploadOKFile() {
+       // Upload cac file dang duoc luu tru tam thoi
+       $postFileName = 'testUploadOKFile.txt';
+       $tmpFileName = tempnam('/tmp', $postFileName);
+       // Ban se tao file file voi noi dung o body
+       file_put_contents($tmpFileName, array (
+           "Hoge\n",
+           "12345\n"
+       ));
+       // cac du lieu khac tu POST
+       $uploadFormData = array (
+           array (
+               'name' => $postFileName,
+               'tmp_name' => $tmpFileName
+           )
+       );
+       // ham nhan thong tin tu Form data, tham chieu de return formdata theo cau truc nhu tren
+       $this->controller->expects($this->any())
+           ->method('getUploadFileParams')
+           ->will($this->returnValue($uploadFormData));
+       // Chạy test
+       $result = $this->testAction('/todo_lists/upload.json', array (
+           'method' => 'post'
+       ));
+       // ket quua/xac nhan
+       $result = $this->vars['response'];
+       $expected = '2 TODO were registed';
+       $this->assertEquals($expected, $result);
+   }
+
+   /**
+    * Test ham upload
+    * Upload file thu 2, minh se nhap 2 TODO, trong do co 1 validation error
+    */
+   public function testUploadOKandNGFile() {
+       //luu tru tam thoi file thu nhat
+       $postFileName1 = 'testUploadOKandNGFile1.txt';
+       $tmpFileName1 = tempnam('/tmp', $postFileName1);
+       //Ban da tao 1 file voi noi dung upload tro thanh bod
+       file_put_contents($tmpFileName1, array (
+           "hoge\n",
+           "12345\n",
+           "12345\n"
+       ));
+       //luu tru tam thoi file upload thu 2
+       $postFileName2 = 'testUploadOKandNGFile2.txt';
+       $tmpFileName2 = tempnam('/tmp', $postFileName2);
+       //Ban da tao 1 file voi noi dung upload tro thanh body
+       file_put_contents($tmpFileName2, array (
+           "fuga\n",
+           "12345\n" // no se xay ra loi
+       ));
+       // nhan data tu POST 
+       $uploadFormData = array (
+           array (
+               'name' => $postFileName1,
+               'tmp_name' => $tmpFileName1
+           ),
+           array (
+               'name' => $postFileName2,
+               'tmp_name' => $tmpFileName2
+           )
+       );
+
+       //Tao TodoListController
+       //Form data acquisition function, the stub to return the form data prepared above
+       $this->controller->expects($this->any())
+           ->method('getUploadFileParams')
+           ->will($this->returnValue($uploadFormData));
+       //Chay test
+       $result = $this->testAction('/todo_lists/upload.json', array (
+           'method' => 'post'
+       ));
+       //Ket qua nhan duoc
+       $result = $this->vars['response'];
+       $this->assertEquals('3 TODO were registered', $result['errors'][0][0]);
+       $this->assertEquals('The following error occurred.', $result['errors'][1][0]);
+        $this->assertEquals('file:testUploadOKandNGFile1.txt - line: 3: [server]There are the same TODO exists in the TODO list.', $result['errors'][2][0]);
+        $this->assertEquals('file:testUploadOKandNGFile2.txt - line: 2: [server]There are the same TODO exists in the TODO list.', $result['errors'][3][0]);  
+                }
+   } 
  • Hàm setUp
    • Thêm cài đặt mock cho hàm getUploadFileParams của TodoListController
  • Hàm testUploadOKFile
    • Đầu tiên mình Test phần upload . Chỉ upload 1 file, hãy chắc chắn rằng TODO của 2 dòng được liệt kê trong file đã được đăng chính xác ở DB.
      • It is stubbed to return the specified information getUploadFileParams function in the test code.
      • getUploadFileParams vẫn chưa được implement, để tổng hợp thông tin nhận từ form của post từ client qua hàm upload, mình dùng $files = $this->request->params['form']; để lấy. Chúng tôi cắt ra chỉ việc xử lý bằng hàm $ files = $ this-> request-> params['form']; để nhận dữ liệu.
    • Sau đó, cũng như hàm testIndex và hàm testView của bài 1, chạy test ở hàm testAction, chúng ta xác nhận kết quả ở hàm assertEquals.
  • Hàm TestUploadOKandNGFile
    • Lần thứ 2 test phần upload. Mình sẽ upload 2 file 1 lần, tuy nhiên mình sẽ cần chắc chắn rằng trong tổng cộng 5 TODO được đăng, 3 cái đã vào DB, 2 cái xuất hiện lỗi validation error (can not be registered TODO of the same content, it could get caught in the rule that).
    • Tạo một method tham chiếu giống hàm testUploadOKFile trên kia. - Cách chạy test, xác nhận kết quả của method tương tự.

app/Model/TodoList.php

Mình sẽ phải sửa thứ tự để test việc gửi dữ liệu vào DB.

app/Model/TodoList.php

〜略〜

    //Rules xac nhan
    //Minh se kiem tra ID cua Assignee co ton tai trong users table hay khong
    public function existsUser($userId){
-       $userModel = new User();
+       $userModel = ClassRegistry::init('User');
        $count = $userModel->find('count', array('conditions'=>array('id'=>$userId), 'recursive' => -1));
        return $count > 0;
    }

 〜以下略〜

Giống như một hàm cho việc validation của bạn, nhưng sẽ có thêm một hàm existsUser để kiểm tra ID đã được xác định của Assignee, để tạp ra một User model,

  • new User()

Đây là hàm set

  • ClassRegistry::init('User')

Sẽ thay cho new User(), nó sẽ lấy thông tin kết nối được set ở biến test của database.php trong quá trình chạy test, cách này dùng mình thấy được thiết lập của default.
Việc tạo Model them cách này tốt hơn cách đầu tiên.

app/Controller/TodoListsController.php

Như đã được nhắc đến trên kia, nó được tham chiếu và fix các bug trong quá trình nhận thông tin từ form được gửi lên từ client.

app/Controller/TodoListsController.php

〜略〜

    }

    public function upload() {
-       $files = $this->request->params['form'];
+       $files = $this->getUploadFileParams();
        $owner = $this->Auth->user()['id'];
        $numTodos = 0;
+       $errors = array ();
        foreach ( $files as $file ) {
            $fileName = $file['name'];
            $filePath = $file['tmp_name'];
            $todos = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
            $assignee = $owner;
-           $errors = array ();
            $lineNo = 1;
            foreach ( $todos as $todo ) {
                $data = array ();
                $data['todo'] = $todo;
                $data['status'] = 0;
                $data['owner'] = $owner;
                $data['assignee'] = $assignee;
                $res = $this->TodoList->save($data);
                if ($res) {
                    $numTodos++;
                } else {
                    if (count($this->TodoList->validationErrors) > 0) {
                        foreach ( $this->TodoList->validationErrors as $validationErrorsOfLine ) {
                            $title = 'file:' . $fileName . ' - line: ' . $lineNo . ': ';
                            foreach ( $validationErrorsOfLine as $validationError ) {
                                $errors[] = array (
                                                $title . $validationError
                                );
                            }
                        }
                    }
                }
                $this->TodoList->create();
                $lineNo++;
            }
        }
        if (count($errors) > 0) {
            $this->TodoList->validationErrors = $errors;
            $response = $this->editResponse(false);
            array_unshift($response['errors'], array (
                            '以下のエラーが発生しました。'
            ));
            if ($numTodos > 0) {
                array_unshift($response['errors'], array (
                                $numTodos . '件のTODOを登録しました。'
                ));
            }
        } else {
            $response = $numTodos . 'TODO were registered';
        }
        $this->set(compact('response'));
        $this->set('_serialize', 'response');
    }

+   protected function getUploadFileParams(){
+       return $this->request->params['form'];
+   }
+
    //edit response
    private function editResponse($res){
        if($res){

〜以下略〜
  • Việc tham chiếu
    • thêm hàm getUploadFileParams, quá trình nhận thông tin trong form gửi lên từ client ($this-> request-> params['form']) sẽ bị ngắt khỏi hàm upload.
  • Fix các bug
    • Chúng ta cần khởi tạo biến $errors để lưu trữ thông tin về các lỗi validation error $errors = array() và vị trí của chúng sẽ không đổi. Và đó chỉ là khi trước khi đến phần chỉnh sửa, những thông tin lỗi của mỗi file sẽ bị xóa. :scream:

Tóm tắt bài học

Chúng ta cần thực hiện các bước sau.

  • :white_check_mark: app/Test/Case/Controller/TodoListsControllerTest.php sửa như hướng dẫn trên.
  • :white_check_mark: app/Model/TodoList.php sửa như hướng dẫn trên.
  • :white_check_mark: implement phần đầu của app/Controller/TodoListsController.phpの.
  • :white_check_mark: Chạy test. Bạn sẽ thấy lỗi và xử lý chúng như hướng dẫn dưới đây.

Test thất bại vì có bug :scream:

phpUnit_6.jpg

Test lại một lần nữa sau khi fix bug

  • :white_check_mark: implement fix các bug vào app/Controller/TodoListsController.php
  • :white_check_mark: Chạy test. Chúc bạn thành công :smile:

test thành công :grin:

phpUnit_7.jpg

  • :white_check_mark: Commit lên Git

:warning: Hiển thị dạng Diff trên Github

第10回 Lesson2 アップロード機能のテスト · suzukishouten-study/rest-study@fa3f13f

Mình sẽ hoàn tất việc refactoring ở Bài 3

:large_blue_circle: Bài 3: Refactoring chức năng upload

Mình chỉ fix trong app/Controller/TodoListsController.php.
Đầu tiên, hãy xem qua về hàm upload trước khi mình thao tác.

File-Truoc-Khi-Refactor
    public function upload() {
        $files = $this->getUploadFileParams();
        $owner = $this->Auth->user()['id'];
        $numTodos = 0;
        $errors = array ();
        foreach ( $files as $file ) {
            $fileName = $file['name'];
            $filePath = $file['tmp_name'];
            $todos = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
            $assignee = $owner;
            $lineNo = 1;
            foreach ( $todos as $todo ) {
                $data = array ();
                $data['todo'] = $todo;
                $data['status'] = 0;
                $data['owner'] = $owner;
                $data['assignee'] = $assignee;
                $res = $this->TodoList->save($data);
                if ($res) {
                    $numTodos++;
                } else {
                    if (count($this->TodoList->validationErrors) > 0) {
                        foreach ( $this->TodoList->validationErrors as $validationErrorsOfLine ) {
                            $title = 'file:' . $fileName . ' - line: ' . $lineNo . ': ';
                            foreach ( $validationErrorsOfLine as $validationError ) {
                                $errors[] = array (
                                                $title . $validationError
                                );
                            }
                        }
                    }
                }
                $this->TodoList->create();
                $lineNo++;
            }
        }
        if (count($errors) > 0) {
            $this->TodoList->validationErrors = $errors;
            $response = $this->editResponse(false);
            array_unshift($response['errors'], array(
                'The following error occurred.'
            ));
            if ($numTodos > 0) {
                array_unshift($response['errors'], array(
                    $numTodos . ' TODO were registered'
                ));
            }
        } else {
            $response = $numTodos . ' TODO were registered';
        }
        $this->set(compact('response'));
        $this->set('_serialize', 'response');
    }

Nó khá là dài dòng, chắc chỉ có người viết mới hiểu chương trình, logic của nó sẽ rất khó để người khác hình dung được.
Mùi này đã được giới thiệu trong phần đầu của sinister smell, mùi có tên là "too long method".
Bây giờ mình sẽ refactor nó!
Nếu làm theo logic này thì chúng có thể thấy nó chia thành các phần như sau:
- Nhận POST data gửi lên khi client thực hiện upload
- Nhận ID của user đang login
- Đọc các file được upload và đăng chúng vào DB thành các TODO
- Lưu trữ các file được upload vào 1 mảng và đọc cùng 1 lần
- Lần lượt Đăng các TODO này vào DB từ mảng lưu trữ.
- Nếu có lỗi validation error, lưu trữ nội dung.
- Hoàn tất upload và Hiển thị kết quả công việc qua message tới client

Hãy chia chúng thành các hàm.
(Cái này không được coi như là một kỹ năng, ...) nhưng đây là một trong những kỹ thuật vơ bản nhất của "Extract Method" trong refactoring!

Hàm sau khi được chia ra và hàm upload được sửa bằng cách "Chia Method" sẽ như sau.

ExtractedFunction

public function upload() {
        $fileUploadParams = $this->getUploadFileParams();
        $loginUserId = $this->getLoginUserId();
        $owner = $loginUserId;
        $assignee = $loginUserId;
        $errors = array();
        $numRegists = $this->registerFilesAsTodos($fileUploadParams, $owner, $assignee, $errors);
        $response = $this->editUploadResponse($numRegists, $errors);
        $this->set(compact('response'));
        $this->set('_serialize', 'response');
    }
    //Lay du lieu POST tu file upload
    protected function getUploadFileParams(){
        return $this->request->params['form'];
    }
    //Lay login User ID
    protected function getLoginUserId(){
        return $this->Auth->user()['id'];
    }
    /Dang ky gui cac file todo de gui vao DB
    private function registerFilesAsTodos($fileUploadParams, $owner, $assignee, &$errors){
        $numRegists = 0;
        //$errors = array();
        foreach ( $fileUploadParams as $fileUploadParam ) {
            $fileName = $fileUploadParam['name'];
            $filePath = $fileUploadParam['tmp_name'];
            $todos = $this->readUploadTodoFile($filePath);
            $numRegists += $this->registerTodos($fileName, $todos, $owner, $assignee, $errors);
        }
        return $numRegists;
    }
    // Doc file upload gui vao mang
    protected function readUploadTodoFile($filePath){
        return file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    }
    // Gui TODO o tren mang vao DB
    private function registerTodos($fileName, $todos, $owner, $assignee, &$errors){
        $numRegists = 0;
        $lineNo = 1;
        foreach ( $todos as $todo ) {
            $record = array ();
            $record['todo'] = $todo;
            $record['status'] = 0;
            $record['owner'] = $owner;
            $record['assignee'] = $assignee;
            $res = $this->TodoList->save($record);
            if ($res) {
                $numRegists++;
            } else {
                $validationErrors = $this->TodoList->validationErrors;
                if (count($validationErrors) > 0) {
                    $this->formatValidationErrorMessage($fileName, $lineNo, $validationErrors, $errors);
                }
            }
            $this->TodoList->create();
            $lineNo++;
        }
        return $numRegists;
    }
    // Cau truc noi dung cho cac thong bao loi validation error
    private function formatValidationErrorMessage($fileName, $lineNo, $validationErrors, &$errors){
        foreach ( $validationErrors as $validationErrorsOfLine ) {
            $title = 'file:' . $fileName . ' - line: ' . $lineNo . ': ';
            foreach ( $validationErrorsOfLine as $validationError ) {
                $errors[] = array (
                    $title . $validationError
                );
            }
        }
    }
    // Cau truc cac tin nhan thong bao ket qua toi client
    private function editUploadResponse($numRegists, $errors){
        if (count($errors) > 0) {
            $this->TodoList->validationErrors = $errors;
            $response = $this->editResponse(false);
            array_unshift($response['errors'], array (
                'The following error occurred.'
            ));
            if ($numRegists > 0) {
                array_unshift($response['errors'], array (
                    $numRegists . ' TODO were registered'
                ));
            }
        } else {
            $response = $numRegists . ' TODO were registered';
        }
        return $response;
    }


Logic đã được mô tả ở trên, bây giờ tôi sẽ show nó ra bảng "Extract Method" dưới đây.

Hành động extracted method
Get POST data khi client chạy Upload getUploadFileParams
Lấy ID của user đang đăng nhập getLoginUserId
Đọc các file được upload và đăng nó vào DB như là TODO registerFilesAsTodos
Lưu trữ các file được upload vào mảng và đọc một lần readUploadTodoFile
Lần lượt từng TODO lưu trên mảng được đăng vào DB registerTodos
Nếu có validation error, tạo các nội dung phản hồi formatValidationErrorMessage
Gửi các message của kết quả upload cho client editUploadResponse

Mỗi tiến trình đều giống như trước khi chỉnh sửa vì chúng ta chỉ phân tách nó ra các method nhỏ.
Truyền vào các arguments và trả về giá trị cho các thông tin được trao đổi.

:warning: Thông tin về validation error sẽ được truyền vào &$errors.

Tóm tắt bài học

Hãy chạy test sau khi mình đã hoàn tất các bước sau.

  • :white_check_mark: app/Controller/TodoListsController.php sửa theo hướng dẫn trên.
  • :white_check_mark: Chạy test, nếu thành công kết quả sẽ như dưới đây.

Test thành công :smile:
phpUnit_8.jpg

  • :white_check_mark: Commit lên Git

:warning: Hiển thị dạng diff trên Github

第10回 Lesson3 アップロード機能のリファクタリング · suzukishouten-study/rest-study@e774e5e

Đó là tất cả bài viết này.

Trailer

Bài tiếp chúng ta sẽ tái cấu trúc ở phía client "refactoring (client ed)".
Tôi hi vọng chúng ta sẽ làm tốt ở phía server như những gì mình đã làm hôm nay.

Rất mong nhận được những phản hồi/góp ý của các bạn.

Cảm ơn các bạn đã dõi theo series này, tôi hi vọng sẽ nhận được những phản hồi, góp ý tích cực từ tất các bạn để có thể làm tốt hơn nữa.

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2