Thứ Tư, 14 tháng 4, 2021

Testing trong AdonisJS





## Test Categories
- Unit Tests: test một phần nhỏ của code.
    Ví dụ test code validation user detail có chạy đúng không.

- Functional Tests: Test một chức năng lớn, như là end-user đang gửi request vậy.


## Setup
https://adonisjs.com/docs/4.1/testing
> adonis install @adonisjs/vow

- Cần thiết lập `VowProvider`.
- thiết lập vowfile.js
- .env.testing

Ví dụ một file test đơn giản
File test/unit/example.spec.js
<!-->
'use strict'

const { test } = use('Test/Suite')('Example')

test('make sure 2 + 2 is 4', async ({ assert }) => {
assert.equal(2 + 2, 4)
})
<-->


## Chạy test
> adonis test


## Suite
Mỗi file test là 1 Suite, được định nghĩa ở trong một group tests.


## Traits
Để tránh các test runner chạy với các chức năng không cần thiết. AdonisJs phân tách các chức năng này nhỏ ra như các traits.
Ví dụ muốn chạy browser thì cần thêm vào
<!-->
const { test, trait } = use('Test/Suite')('User registeration')

trait('Test/Browser') // sẽ load các thư viện cần thiết liên quan.

test('return error when credentials are wrong', async ({ browser }) => {
const page = await browser.visit('/user')
})
<-->


## Lifecycle Hooks
Thêm vào các xử lý init data trước khi chạy hoặc revert lại data sau khi chạy...


## Assertions
`assert object` là một ánh xạ của chaijs/assert. Dùng so sánh kết quả mong đợi và kết quả thực.


## HTTP Tests
Dùng để test API.
Ví dụ: xác thực API, dữ liệu trả về mong muốn của 1 API...
> Tạo 1 file test mới: `adonis make:test Post`
> chạy test: `adonis test functional`


## Browser Tests
AdonisJs makes it simple to write functional tests using the `Chrome browser`.
Under the hood, it uses `Puppeteer` to launch a web browser and run assertions.

- Setup:
<!-->
adonis install @adonisjs/vow-browser

# Skip Chromium download
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true adonis install @adonisjs/vow-browser
<-->

- Đăng kí
start/app.js
<!-->
const aceProviders = [
'@adonisjs/vow-browser/providers/VowBrowserProvider'
]
<-->

> adonis test functional


## Tạo giả lập
1. Mail Fake
2. Events Fake


Redis trong AdonisJS




## Redis
1. Cài đặt
> adonis install @adonisjs/redis

2. Đăng kí RedisProvider
Chỉnh sửa file `start/app.js`

const providers = [
'@adonisjs/redis/providers/RedisProvider'
]

3. Cấu hình redis
Chỉnh sửa ở `config/redis.js`


## Basic Use
File `app/Controllers/Http/UserController.js`
<!-->
'use strict'

const Redis = use('Redis')
const User = use('App/Models/User')

class UserController {

async index () {
const cachedUsers = await Redis.get('users')
if (cachedUsers) {
return JSON.parse(cachedUsers)
}

const users = await User.all()
await Redis.set('users', JSON.stringify(users))
return users
}
}
<-->


## Redis Commands
https://redis.io/commands
<!-->
const Redis = use('Redis')

const user = {
username: 'foo',
email: 'foo@bar.com'
}

// set user
await Redis.hmset('users', user.username, JSON.stringify(user))

// get user
const user = await Redis.hmget('users', user.username)
<-->


## Pub/Sub
Redis Provider hỗ trợ publish/subscribe (pub/sub) để chia sẻ tin nhắn trên cùng một server hoặc băng qua nhiều server
Sử dụng cho các dịch vụ tin nhắn, chat message.
khi các thành viên cùng tham gia vào 1 kênh(`subscribe`),
thì khi một thành viên bất kì gửi tin nhắn(`publish`) thì các thành viên khác trong kênh đều nhận
được message ngay.

1. Đăng kí một kênh
start/redis.js
<!-->
'use strict'

const Redis = use('Redis')

Redis.subscribe('music', async (track) => {
console.log('received track', track)
})
<-->

Tạo một file `start/redis.js` nếu nó không tồn tại và load nó ở file `server.js`:
> new Ignitor(require('@adonisjs/fold')).preLoad('start/redis')

2. Khi một kênh đã được đăng kí thì có thể sử dụng như bên dưới
> const Redis = use('Redis')
> Redis.publish('music', track)

3. Một số method sẵn có
3. a) - `subscribe(channel, listener)`
<!-->
Redis.subscribe('music', (track) {
console.log(track)
})
<-->

Bạn cũng có thể thêm vào theo dạng
> Redis.subscribe('music', 'Music.newTrack')

Và tạo mới file app/Listeners/Music.js để xử lý
<!-->
'use strict'

const Music = exports = module.exports = {}

Music.newTrack = (track) => {
console.log(track)
}
<-->

3. b) - `psubscribe(pattern, listener)`
Subscribe đến một mẫu(sử dụng dấu ?):
<!-->
Redis.psubscribe('h?llo', function (pattern, message, channel) {
})
<-->
==>
Redis.publish('hello')
Redis.publish('hallo')

3. c) - `publish(channel, message)`
Gửi tin nhắn đến 1 kênh đã đăng ký
<!-->
Redis.publish('music', JSON.stringify({
id: 1,
title: 'Love me like you do',
artist: 'Ellie goulding'
}))
<-->

3. d) - `unsubscribe(channel)`
Hủy đăng kí một kênh. Ex: Redis.unsubscribe('music')

3. e) - `punsubscribe(channel)`
> Redis.punsubscribe('h?llo')


## Thiết kế liên kết nhiều redis server
File `config/redis.js`
<!-->
module.exports = {
connection: 'local',

local: {
...
},

secondary: {
host: 'myhost.com',
port: 6379
}
}
<-->

1. Kết nối một redis connection
<!-->
await Redis.connection('secondary').get('users')
OR
// hold reference to connection
const secondaryConnection = Redis.connection('secondary')
await secondaryConnection.get('users')
<-->

2. Hủy bỏ kết nối
> await Redis.quit('primary')
> await Redis.quit(['primary', 'secondary'])



View Engine trong AdonisJS




Adonis JS sử dụng Edge view engine.
Edge supports:
- Layouts & partials
- Components
- Runtime debugging using chrome dev tools
- Logical tags and everything in between


## Cơ bản

1. Thiết lập để sử dụng Edge
File start/app.js
const providers = [
'@adonisjs/framework/providers/ViewProvider'
]

Chú ý là tất cả các views của bạn sẽ lưu ở folder resources/views, với .edge extensions.

2. Tạo mới 1 view
> adonis make:view welcome
==> resources/views/welcome.edge

3. render một trang tĩnh
Route.get('welcome', ({ view }) => {
return view.render('welcome')
})

4. sub folder
File path: resources/views/my/nested/view.edge
> view.render('my.nested.view')


## Request information
Tất cả views đều có thể truy cập đến Request object

> The request URL is {{ request.url() }}

Chú ý là `{{ request.url() }}` cũng có thế thay thế bằng `{{ url }}`


## Các hàm Globals
1. style
> {{ style('style') }}
==> Render: <link rel="stylesheet" href="/style.css" />

> {{ style('https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css') }}
==> Render:
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" />

2. script
> {{ script('app') }}
==> <script type="text/javascript" src="/app.js"></script>

> {{ script('https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js') }}
==>
<script type="text/javascript"
    src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>

3. assetsUrl
Trả về đường dẫn có thư mục gốc là thư mục public
> <img src="{{ assetsUrl('images/logo.png') }}" />
==> Render: <img src="/images/logo.png" />
==> File vật lý: public/images/logo.png

4. route
Ví dụ route: Route.get('users/:id', 'UserController.show') .as('profile')
> <a href="{{ route('profile', { id: 1 }) }}"> View profile </a>
> <a href="{{ route('UserController.show', { id: 1 }) }}"> View profile </a>

5. url
Lấy ra request url hiện đang xử lý
> The request URL is {{ url }}

6. auth
Nếu bạn đang sử dụng Auth Provider thì có thể lấy thông tin user thông qua function auth
> {{ auth.user }}

7. CSRF
Nếu bạn đang sử dụng Shield Middleware thì có thể sử dụng function này

> {{ csrfToken }}
==> Output token value

> {{ csrfField() }}
==> <input type="hidden" name="_csrf" value="...">

Nếu đang sử dụng Shield Middleware thì CSP headers sẽ được thêm tự động.
Tuy nhiên bạn cũng có thể tự thêm vào thông qua cspMeta global
<head> {{ cspMeta() }} </head>


## Tags
Ví dụ @if, @each, @include ...
Đọc thêm: http://edge.adonisjs.com/docs/tags

Một số tags đặc biệt:

1. loggedIn
<!-->
@loggedIn
You are logged in!
@else
<a href="/login">Click here</a> to login.
@endloggedIn
-->

2. Render file svg từ thư mục public với `inlineSvg`
> <a href="/login"> @inlineSvg('lock') Login </a>


## Mở rộng Views
Viết các function xử lý mở rộng cho view và đăng kí ở providers hay Ignitor hooks.

1. Tạo các hàm Globals
const View = use('View')
View.global('currentTime', function () {
return new Date().getTime()
})

==> Sử dụng trong views: `{{ currentTime() }}`

2. Sử dụng this để tương tác với các đối tượng global
<!-->
View.global('subMitbutton', function (text) {
return this.safe(`<button type="submit">${text}</button>`)
})
-->

Chú ý ở đây sử dụng hàm safe để trả về HTML không bị mã hóa.

b) Sử dụng this.resolve để lấy kết quả trả về từ các global functions khác
<!-->
View.global('messages', {
success: 'This is a success message',
warning: 'This is a warning message'
})

View.global('getMessage', function (type) {
const message = this.resolve('messages') // lấy object messages từ global messages
return messages[type]
})
-->
==> Gọi ở view: `{{ getMessage('success') }}`

3. Viết thêm các Tags
<!-->
const View = use('View')
class MyTag extends View.engine.BaseTag {
//
}
View.engine.tag(new MyTag())
-->

4. Thêm các global value dựa theo các request đặc biệt
Tạo các middleware để bắt các request đặc biệt và cập nhật global value vào views
<!-->
class SomeMiddleware {

async handle ({ view }, next) {
view.share({
apiVersion: request.input('version')
})

await next()
}
}
-->
==> Sử dụng ở view: `{{ apiVersion }}`


Thứ Ba, 13 tháng 4, 2021

Seeds và Factories trong AdonisJS




## Seeds
1. Tạo mới file Seeds
> adonis make:seed User

2. Chạy tất cả các file seed
> adonis seed

Cần kết hợp với Factory để mở rộng tính năng

3. Chỉ chạy 1 file seed
> Ex: adonis seed --files UserSeeder.js --force


## Factories
Tạo nhanh dữ liệu test cho các bảng.

Ở file database/factory.js tạo kết nối với Model User
<!-->
const Factory = use('Factory')
const Hash = use('Hash')

Factory.blueprint('App/Models/User', async (faker) => {
return {
username: faker.username(),
email: faker.email(),
password: await Hash.make(faker.password())
}
})
<-->

==>
Ở file seeds/UserSeeder.js
Thêm vào `const user = await Factory.model('App/Models/User').create()`
để tạo ra 1 user ngẫu nhiên

Tạo nhanh 5 users:
> const usersArray = await Factory.model('App/Models/User').createMany(5)


## Tạo liên kết giữa các bảng
1. Tạo file app/Models/Post.js
<!-->
'use strict'

/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
const Model = use('Model')

class Post extends Model {
static boot () {
super.boot()
}
}

module.exports = Post
<-->

2. Tạo file migration for Post
> adonis make:migration post
==> Output: 1616983081702_post_schema.js
Chỉnh sửa thêm như bên dưới:
<!-->
'use strict'

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use('Schema')

class PostSchema extends Schema {
up () {
this.drop('posts')
this.create('posts', (table) => {
table.increments()
table.string('title', 250).notNullable()
table.text('body').notNullable()
table.timestamps(),
table.integer('user_id')
})
}

down () {
this.drop('posts')
}
}

module.exports = PostSchema
<-->

3. Chỉnh sửa file database/factory.js
<!-->
// User blueprint
Factory.blueprint('App/Models/User', (faker) => {
return {
username: faker.username(),
password: faker.password()
}
})

// Post blueprint
Factory.blueprint('App/Models/Post', (faker) => {
return {
title: faker.sentence(),
body: faker.paragraph()
}
})
<-->

4. Chỉnh sửa ở file UserSeeder.js
<!-->
/** @type {import('@adonisjs/lucid/src/Factory')} */
const Factory = use('Factory')

class UserSeeder {
async run () {
// const users = await Database.table('users')
// console.log(users)

// const user = await Factory.model('App/Models/User').create()
// const usersArray = await Factory.model('App/Models/User').createMany(5)

const user = await Factory.model('App/Models/User').create()
const post = await Factory.model('App/Models/Post').make()
await user.posts().save(post)
}
}

module.exports = UserSeeder
<-->

5. Run command chạy tất cả các file seeds
> adonis seed


## Model Factory API
Bên dưới là danh sách các method sẵn có khi sử dụng `Lucid model` factories.

1. `create()`
> await Factory.model('App/Models/User').create()

2. `createMany(n)`
> await Factory.model('App/Models/User').createMany(3)

3. `make`
Trả về một liên kết của 1 model(`model instance`), chứ không tạo dữ liệu vào DB
> await Factory.model('App/Models/User').make()

4. `makeMany(n)`
> await Factory.model('App/Models/User').makeMany(3)


## Không sử dụng Lucid models
Lucid liên kết với DB thông qua các model.
Nếu dự án của bạn không sử dụng `Lucid models`, thì vẫn có thể sử dụng `Database Provider`.

1. blueprint
<!-->
Factory.blueprint('users', (faker) => {
return {
username: faker.username(),
password: faker.password()
}
})
<-->

2. create
<!-->
run () {
await Factory.get('users').create()
}
<-->

3. table
Đổi tên table trong lúc chạy
> await Factory.get('users').table('my_users').create()

4. returning
Đối với PostgreSQL, có thể định nghĩa column sẽ trả về
> await Factory.get('users').returning('id').create()

5. connection
Đổi kết nối DB trong lúc chạy
> await Factory.get('users').connection('mysql').returning('id').create()

6. createMany
> await Factory.get('users').createMany(3)


## Custom Data
methods make, makeMany, create và createMany chấp nhận các filter.
<!-->
const user = await Factory
.model('App/Models/User')
.create({ status: 'admin' })
-->

<!-->
Factory.blueprint('App/Models/User', async (faker, i, data) => {
return {
username: faker.username(),
status: data.status
}
})
<-->



Migrations trong AdonisJS




## Migrations
Để sử dụng Migrations, cần khai báo `Migrations Provider` ở file `start/app.js`.

Mặc định sẽ có sẵn 2 file migration ở database/migrations
1. Chạy các files bên trong thư mục database/migrations
> adonis migration:run

Các file nào đã chạy rồi thì lần sau khi gõ command trên nó sẽ không chạy lại các files đó.

2. Kiểm tra status của các migration
> adonis migration:status

3. Nếu có sai sót thì rollback trở lại bản trước khi chạy `migration:run`
> adonis migration:rollback

4. Tạo file migration mới:
> adonis make:migration users
==> Output: database/migrations/1502691651527_users_schema.js

## Cấu trúc file Migration (Schema Files)
Một schema file bao gồm 2 phương thức là up và down.

1. `up()`
Phần xử lý chính (tạo mới table, chỉnh sửa field..)

2. `down()`
Được dùng để revert lại các thay đổi do phần `up()` tạo ra.

## Kiểu dữ liệu của một cột
Method Description
table.bigInteger(name) Adds a bigint column.
table.binary(name, [length]) Adds a binary column.
table.boolean(name) Adds a boolean column.
table.date(name) Adds a date column.
table.datetime(name, [precision]) Adds a datetime column.
table.decimal(name, [precision], [scale]) Adds a decimal column.
table.enu(col, values, [options]) Adds a enum column.
table.float(name, [precision], [scale]) Adds a float column.
table.increments(name) Adds an auto incrementing column.
table.integer(name) Adds an integer column.
table.json(name) Adds a json column.
table.string(name, [length=255]) Adds a string column.
table.text(name, [textType]) Adds a text column.
table.time(name, [precision]) Adds a time column.
table.timestamp(name, [useTz], [precision]) Adds a timestamp column.
table.timestamps([useTimestamps], [defaultToNow]) Adds created/updated columns.
table.uuid(name) Adds a uuid column.

## Column Modifiers
https://adonisjs.com/docs/4.1/migrations
Document: https://knexjs.org/#Schema-after

Method Description
.after(field) Set column to be inserted after field.
.alter() Marks the column as an alter/modify.
.collate(collation) Set column collation (e.g. utf8_unicode_ci).
.comment(value) Set column comment.
.defaultTo(value) Set column default value.
.first() Set column to be inserted at the first position.
.index([indexName], [indexType]) Specifies column as an index.
.inTable(table) Set foreign key table (chain after .references).
.notNullable() Set column to not null.
.nullable() Set column to be nullable.
.primary([constraintName]) Set column as the primary key for a table.
.references(column) Set foreign key column.
.unique() Set column as unique.
.unsigned() Set column to unsigned (if integer).

## Sử dụng một connection khác
<!-->
const Schema = use('Schema')
class UsersSchema extends Schema {
static get connection () {
return 'mysql'
}

// ...
}
module.exports = UsersSchema
<-->

## Migration Commands
Command Description
`make:migration` Create a new migration file.
`migration:run` Run all pending migrations.
`migration:rollback` Rollback last set of migrations.
`migration:refresh` Rollback all migrations to the 0 batch then re-run them
`migration:reset` Rollback all migrations to the 0 batch.
`migration:status` Get the status of all the migrations.

- Command Help: `adonis migration:run --help`

## Danh sách schema method sẵn có
1. Tạo bảng mới
> this.create('users', (table) => { .. })

2. Tạo bảng mới nếu nó chưa tồn tại
> this.createIfNotExists('users', (table) => { .. })

3. Đổi tên table
> this.rename('users', 'my_users')

4. Xóa một table
> this.drop('users')

5. Xóa table nếu đã tồn tại
> this.dropIfExists('users')

6. Chỉnh sửa table
<!-->
this.alter('users', (table) => {
// add new columns or remove existing
})
<-->

7. SQL query thuần (raw)
<!-->
up () {
this
.raw("SET sql_mode='TRADITIONAL'")
.table('users', (table) => {
table.dropColumn('name')
table.string('first_name')
table.string('last_name')
})
}
<-->

8. Kiểm tra một table tồn tại hay không
> const exists = await this.hasTable('users')

9. set timestamp với `knex.fn.now()`
> table.timestamp('created_at').defaultTo(this.fn.now())'


## Chạy code tùy ý
<!-->
class UserSchema {
up () {
// create new table
this.create('new_users', (table) => {
})

// copy data
this.schedule(async (trx) => {
const users = await Database.table('users').transacting(trx)
await Database.table('new_users').transacting(trx).insert(users)
})

// drop old table
this.drop('users')
}
}
-->



Học lập trình web căn bản với PHP

Bài 1: Các kiến thức căn bản Part 1:  https://jimmyvan88.blogspot.com/2012/05/can-ban-lap-trinh-web-voi-php-bai-1-cac.html Part 2:  https://...