วันที่สองสรุปเนื้อหาไปเรียน Anular for B+ at Geeky Base

Phatcharaphan Ananpreechakun
odds.team
Published in
9 min readJul 16, 2018

จบคอร์สแล้วว วันสุดท้ายที่ได้มีโอกาสไปเรียนเขียน Angular version 6 ถึงจะไปสาย แต่ก็ยังได้ไปเรียนน๊า วันนี้กินข้าวฟรีอีกแล้วว ขอบคุณผู้สนับสนุนเกือบทุกคอร์สที่ไปเรียนที่ Geeky Base พี่รูฟ วันนี้เพิ่งรู้ว่า ติ่ง BNK48 เป็นผู้อยู่เบื้องหลังในการสอนครั้งนี้คือ เจ ผู้ที่ช่วย พี่อูกับพี่ตั้ม สอนในครั้งนี้ มีข้อมูลที่เราได้เรียนกัน การเรียนวันนี้รู้สึกว่า สองคนที่สอนชั่งเข้าขากันดีจริงๆ ตบมุขกันตลอด ฮ่ากันได้ตลอด มีคอร์สสอนครั้งหน้าไม่พลาดแน่ ความรู้เพียบ (ขอยืมรูปมาลงหน่อยยน๊าาาาาา)

ใครยังไม่ได้อ่านของวันแรกสามารถ อ่านได้ที่นี่

เนื้อหาที่สอนวันที่สองมีดังนี้ (อาจะมีมากกว่านี้ แต่เท่าที่จำได้มีเท่านี้น๊าา)

  • ReactiveForms, FormBuilder, Validators, Custom Validators
  • Interceptor, Guard, Unit Test
  1. มาเริ่มกันที่ ReactiveForms กันเลยย เราไปสร้าง component ชื่อว่า login
ng g c components/login
  • หลังจากที่สร้าง component แล้ว ในการสร้าง Form Login ทางผู้สอนจะใช้ ReactiveForms เพื่อมาช่วยอำนวยความสะดวกให้สามารถโต้ตอบกันระหว่างข้อมูลที่ส่งมาจากฝั่ง server กับส่วนติดต่อผู้ใช้ในรูปแบบ Form
  • ให้เราไปที่ไฟล์ app.module.ts เพื่อทำการ import ReactiveForms ใน imports ด้วยคำสั่ง
ReactiveFormsModule

อย่าลืม import

import { ReactiveFormsModule } from “@angular/forms”;

  • หลังจากที่เรา import ReactiveFormsModule แล้วเราก็สามารถที่จะใช้งาน ReactiveForms ได้ล่ะ ต่อมาให้สร้างตัวแปรสำหรับ FormGroup เพื่อใช้ในการติดตาม value และ validity state ของ Login Form ของเรา ให้ไปที่ไฟล์ login.component.ts

Ref : https://angular.io/guide/reactive-forms

loginForm: FormGroup;

อย่าลืม import

import { FormGroup} from “@angular/forms”;

  • หลังจากที่เราสร้างตัวแปร FormGroup แล้วให้เรา inject FormBuilder ใน constructor เพื่อช่วยในการสร้าง FormController แต่ละอันให้ ให้ไปที่ไฟล์ login.component.ts
private formBuilder: FormBuilder

อย่าลืม import

import { FormGroup, FormBuilder } from “@angular/forms”;

  • หลังจากที่เรา Inject FormBuilder มาแล้วให้เราสร้าง factory method สำหรับสร้าง FormGroup ให้ไปที่ไฟล์ login.component.ts
this.loginForm = this.formBuilder.group({login: [""],password: [""]});
  • หลังจากที่เราสร้างตัวแปรต่างๆที่ใช้ใน Form ต่อมาให้สร้าง Form สำหรับ login และเรียกใช้ FormGroup ตัวแปรที่เราสร้างไว้ ซึ่งตัวแปรที่เราสร้างไว้นั้นจะ Referenceใน formControlName ไปที่ไฟล์ login.component.html
<div class="container">
<div class="card">
<div class="card-body">
<h1>เข้าสู่ระบบ</h1>
<form [formGroup]="loginForm" class="form">
<div class="form-group">
<label for="login">ชื่อผู้ใช้ : </label>
<input type="text" id="login" class="form-control" formControlName="login" placeholder="ชื่อผู้ใช้งาน">
</div>
<div class="form-group">
<label for="password">รหัสผ่าน : </label>
<input type="text" id="password" class="form-control" formControlName="password" placeholder="รหัสผ่าน">
</div>
<button type="submit" (click)="login()" class="btn btn-primary">เข้าสู่ระบบ</button>
</form>
</div>
</div>
</div>
  • จะเห็นว่าเมื่อเราสร้าง Form สำหรับ Login แล้วจะมีปุ่มเพื่อใช้ในการ Login ให้เราไปสร้าง Method ชื่อ login() และลอง Print ข้อมูลมาดูใน Console จะเห็นว่า Value ที่เรากรอกใน Input จะออกมาเป็น Object ที่เราได้ Define ไว้ใน FormBuilder ในไฟล์ login.component.ts
login() {console.log(this.loginForm.value);
}
  • ต่อมาให้เราสร้าง Service สำหรับ login ด้วยคำสั่ง
ng g s services/authentication
  • ในวันแรกเรามี API ให้เราได้ใช้งานซึ่งก็จะมี API สำหรับ Login ซึ่งมีข้อมูลดังนี้ response ที่ได้จากการ Login จะเป็น Token
  • ไปที่ไฟล์ authentication.service.ts สำหรับสร้าง Method Login ให้ inject private http: HttpClient ใน constructor
authLogin(data: User): Observable<String> {return this.http.post<String>(`${environment.api_url}/auth/login`, data);}

อย่าลืม import

import { HttpClient } from ‘@angular/common/http’;

import { Observable } from ‘rxjs’;

import { environment } from ‘src/environments/environment’;

  • เมื่อเรามี service สำหรับ login แล้วให้ไปที่ไฟล์ login.component.ts และเรียก service login ให้ inject AuthenticationService ใน constructor

private authenticationService: AuthenticationService

ในส่วนของ service login ให้ส่ง value ที่มีการกรอกที่หน้าจอใน method ด้วยย

และหลังจาก login สำเร็จให้ Redirect ไปที่ admin

login() {this.authenticationService.authLogin(this.loginForm.value).subscribe((response) => {this.router.navigate(["/admin"]);});}

อย่าลืม inject

private router: Router ใน constructor

และ import

import { Router } from ‘@angular/router’;

  • ต่อมาให้สร้าง Component ชื่อ admin ด้วยคำสั่ง
ng g c components/admin
  • เมื่อเราได้ Component ชื่อ admin แล้วก็ไปสร้าง Routes สำหรับเพิ่มสร้างทางใหม่ ให้ไปที่ไฟล์ app.module.ts
{ path: 'admin', component: AdminComponent},
  • ลองเล่นหน้าจอ Project ของเราจะเห็นว่า เมื่อมีการ Login สำเร็จจะไปยังหน้า admin component
  • ในหน้า admin component ให้ดึงข้อมูลมาแสดงเป็น table ให้ไปที่ไฟล์ admin.component.ts แล้วทำการดึง list ของ Bnk48 มาแสดง ซึ่งในวันแรกเราได้สร้าง service ไว้แล้ว
import { Component, OnInit } from '@angular/core';import { BnkService } from 'src/app/services/bnk.service';import { Member } from 'src/app/models/member';@Component({selector: 'app-admin',templateUrl: './admin.component.html',styleUrls: ['./admin.component.css']})export class AdminComponent implements OnInit {members: Member[];constructor(private bnkService: BnkService) { }ngOnInit() {this.bnkService.list().subscribe((response) => {this.members = response;});}}
  • เมื่อเราได้ข้อมูลของ BNK48 มาทั้งหมดแล้ว ให้นำไปแสดงใน table และมีปุ่มสำหรับ แก้ไข ด้วยให้ไปที่ไฟล์ admin.component.html
<div class="container">
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Image URL</th>
<th>Instagram ID</th>
<th>Action</th>
</tr>
</thead>
<tbody *ngFor="let member of members">
<tr>
<td>{{member.name}}</td>
<td>{{member.imgUrl}}</td>
<td>{{member.instagramId}}</td>
<td>
<a [routerLink]="['/admin/edit/', member._id]">Edit</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
  • จะเห็นว่า เมื่อมีการกด edit จะไปยังเส้นทาง /admin/edit/ ตามด้วย ID ของ BNK48 ซึ่งเส้นทางนี้เรายังไม่มีให้ไปสร้าง component ชื่อว่า edit
ng g c components/edit
  • หลังจากที่เราสร้าง component ชื่อ edit แล้ว ก็ไปเพิ่มเส้นทางในไฟล์ app.module.ts
{ path: 'admin/edit/:id', component: EditComponent}
  • ให้เราไปที่ไฟล์ edit.component.ts เพื่อดึงข้อมูล By id มาแสดงใน Form เพื่อใช้ในการแก้ไข

inject ActivatedRoute ที่ constructor เพื่อดึงข้อมูลใน path ที่เราส่งมาด้วย id

private activatedRoute: ActivatedRoute

อย่าลืม import

import { ActivatedRoute, Router } from ‘@angular/router’;

  • ต่อมาให้สร้าง FormGroup และ FormBuilder ที่ไฟล์ edit.component.ts อย่างแรกก็ inject FormBuilder ใน constructor
private fb: FormBuilder

สร้างตัวแปรสำหรับ FormGroup

adminForm: FormGroup;
  • เรายังไม่มี service สำหรับดึงข้อมูล by id เลย ให้ไปที่ไฟล์ bnk.service.ts เพื่อสร้าง method สำหรับดึงข้อมูล by id
admin(id: string):Observable<Member> {return this.http.get<Member>(`${environment.api_url}/bnk/members/${id}`);}
  • เมื่อมี service สำหรับดึงข้อมูลแล้วให้ไปที่ไฟล์ edit.component.ts เมื่อมีการเรียกใช้ component นี้ให้เรียก service แล้วแสดงข้อมูลใน form แสดงว่า เราต้อง call service ที่ ngOnInit หลังจากที่ call service สำหรับก็จะ set ข้อมูลเข้าไปยัง FormBuilder
ngOnInit() {this.bnkService.admin(this.activatedRoute.snapshot.params.id).subscribe((response) => {this.adminForm = this.fb.group({_id: [response._id],name: [response.name],imgUrl: [response.imgUrl],instagramId: [response.instagramId]});});
}
  • เมื่อมีการ set ข้อมูลใน this.adminForm แล้วก็จะนำข้อมูลมาแสดงที่ไฟล์ edit.component.html และเราจำเป็นที่จะต้อง validate ข้อมูลใน this.adminForm ว่าถ้ามีข้อมูล form ดึงจะแสดงออกมา ด้วยคำสั่ง *ngIf=”adminForm”
<div class="container"><div class="card"><div class="card-body"><form [formGroup]="adminForm" *ngIf="adminForm"><div class="form-group"><label for="name">Name : </label><input type="text" id="name" class="form-control" formControlName="name" placeholder="Name"></div><div class="form-group"><label for="imgUrl">Image Url : </label><input type="text" id="imgUrl" class="form-control" formControlName="imgUrl" placeholder="Image Url"></div><div class="form-group"><label for="instagramId">Instagram Id : </label><input type="text" id="instagramId" class="form-control" formControlName="instagramId" placeholder="Instagram Id"></div><div class="form-group"><button type="submit" class="btn btn-primary">Submit</button>&nbsp;<button type="button" (click)="reset()" class="btn btn-danger">Reset</button></div></form></div></div></div>
  • และเมื่อมีการกด Reset ข้อมูลเดิมที่ดึงมาก็จะแสดง ให้ไปที่ไฟล์ edit.component.ts ให้สร้างตัวแปรที่มี type เป็น Member
member: Member;
  • หลังจากที่มี response จาก service ให้นำตัวแปร member ไปรับข้อมูล
this.member = response;
  • ต่อมาก็สร้าง method reset ที่ไฟล์ edit.component.ts
reset() {this.adminForm.reset(this.member);}
  • ลองเข้าไปเล่นที่ Browser ที่เรา run project ไว้จะเห็นว่า เมื่อมีการแก้ไขข้อมูลแล้ว กด reset ข้อมูลก่อนหน้าที่ยังไม่ได้แก้ไขจะกลับมาเหมือนเดิม
  • ต่อมาอยากให้เมื่อมีการแก้ไขก็จะมีการ update ข้อมูลด้วย
  • ให้เพิ่ม service สำหรับ update ที่ไฟล์ bnk.service.ts
update(data: Member):Observable<Member> {return this.http.patch<Member>(`${environment.api_url}/bnk/members/${data._id}`, data);}
  • เมื่อมี service สำหรับ update แล้วก็จะต้องสร้าง event ในไฟล์ edit.component.html ที่ Submit ให้ชื่อ event ว่า update()
<button type="submit" (click)="update()" class="btn btn-primary">Submit</button>
  • ต่อไปให้ไปสร้าง method ในไฟล์ edit.component.ts และเมื่อ update สำหรับก็จะให้ redirect ไปที่ /admin
update() {this.bnkService.update(this.adminForm.value).subscribe(() => {this.router.navigate(["/admin"]);});}

2. หลังจากที่เราสร้าง form แล้ว ถ้าไม่มีการกรอกข้อมูลลงใน input ก็จะสามารถที่จะ update ได้ แต่มี requirement ใหม่บอกว่า ถ้าไม่กรอกข้อมูลก็จะไม่สามารถที่จะ update ข้อมูลได้ ซึ่งต่อมาเราจะนำ Validators มาใช้งานใน FormBuilder

  • ให้ไปที่ไฟล์ edit.component.ts และเรียกใช้ Validators ใน FormBuilder
Validators.required

อย่าลืม import

import { FormGroup, FormBuilder, Validators } from ‘@angular/forms’;

  • และให้ไป validate ก่อน ถ้ามีข้อมูลครบทุก fields ถึงจะสามารถ update ข้อมูลได้ที่ไฟล์ edit.component.ts ซึ่งเราจะใช้ valid เพื่อบอกว่ามีข้อมูลครบทุก fields แล้วน๊าา
this.adminForm.valid

3. เราจะนำ Custom Validators มาใช้เพื่อบอกว่าถ้า field url ไม่ข้อมูลที่เริ่มต้นด้วย http:// จะไม่สามารถที่จะกรอกได้

ng g class validators/url

เราจะได้ไฟล์ url.service.ts มาให้แก้ไขเป็น url.validator.ts เพราะว่าอยากได้ type ที่เป็น validator ใน angular cli ยังไม่มีให้ระบุ type ในการสร้างไฟล์ (หรือว่ามีใครพอรู้มาบอกทีน๊าาาาาา)

  • เมื่อเราได้ไฟล์ url.validator.ts แล้วให้เขียน validate บอกว่า ถ้ามีการกรอก url ที่เราต้นด้วย http:// ก็จะส่ง null ไปจะแสดงว่า สามารถกรอก url นี้ได้ แต่ถ้ามีการกรอก url ที่ไม่ได้ขึ้นต้นด้วย http:// จะมี information ไปแจ้งบอกว่า กรอกผิดน๊า
import { AbstractControl, ValidationErrors } from "@angular/forms";export class UrlValidator {static validate(control: AbstractControl): ValidationErrors | null {const url : string = String(control.value);if(url.startsWith('http://') || url.startsWith("https://")){return null;}return { 'url': 'เฮ้ยกรอก URL ผิดน๊าาา'}}}
  • เมื่อสร้าง Custom Validators แล้วก็นำไปใช้กันที่ไฟล์ edit.component.ts ใน FormBuilder ต่อจาก Validators

4. ต่อมาเรามาใช้ Interceptor เพื่อช่วยในการดัก http ที่ตัวที่จะเข้ามาใน Project ของเรา ซึ่งเมื่อเรา login เสร็จแล้วเราจะได้ token เพื่อนำไป update ข้อมูล ซึ่งเราจะนำ Interceptor เข้ามาช่วย set token ใน http ให้เรา

  • ให้สร้างไฟล์ token
ng g s interceptors/token

เราจะได้ไฟล์ token.service.ts มาให้แก้ไขเป็น token.interceptor.ts เพราะว่าอยากได้ type ที่เป็น interceptor ใน angular cli ยังไม่มีให้ระบุ type ในการสร้างไฟล์ (หรือว่ามีใครพอรู้มาบอกทีน๊าาาาาา)

  • ไปที่ไฟล์ token.interceptor.ts เพื่อทำการ set token header Authorization ไว้ใน api ที่ยังสร้างขึ้น ซึ่งใน project นี้จะมี 2 url อัน api_url จะเป็น api ที่เราสร้างขึ้น อีกอันจะเป็นของ BNK48 ที่เราไปใช้ของเขาซึ่ง interceptor นี้จะมีการ validate แค่ api_url
import { Injectable } from '@angular/core';import { HttpInterceptor, HttpHandler, HttpEvent, HttpRequest } from '@angular/common/http';import { Observable } from 'rxjs';import { environment } from '../../environments/environment';import { AuthenticationService } from '../services/authentication.service';@Injectable({providedIn: 'root'})export class TokenInterceptor implements HttpInterceptor {constructor(private authenticationService: AuthenticationService) { }intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {const token = this.authenticationService.getToken();if (token && request.url.startsWith(environment.api_url)) {request = request.clone({setHeaders: {Authorization: `Bearer ${token}`}})}return next.handle(request);}}
  • จะเห็นว่า method getToken เรายังไม่มีให้ไปที่ไฟล์ authentication.service.ts เพื่อที่จะสร้าง getToken, setToken ให้สร้างตัวแปร token
private token: string;setToken(token: string) {this.token = token;}getToken(): string {return this.token;}
  • เมื่อมี method setToken ก็ไปที่ login.component.ts เพื่อ set token

5. จะเห็นว่าแต่ละ url เราสามารถที่จะเข้าไปได้โดยตรงได้เลย ถ้าแม้จะยังไม่ login ต่อมาใช้ Guard เพื่อเข้ามาป้องกันในการเข้าใช้งานแต่ละเส้นทางที่เราสร้างขึ้นมา ถ้าไม่มี token เราก็จะไม่ให้เข้าไป

  • ให้สร้างไฟล์ auth
ng g g guard/auth
  • ไปที่ไฟล์ auth.guard.ts จะมีการ validate ว่า ถ้ามี token ก็จะ return true เพื่อให้สามารถเข้าไปใช้งานได้ แต่ถ้าไม่มี token ก็ให้ redirect ไปที่ login
import { Injectable } from '@angular/core';import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';import { Observable } from 'rxjs';import { AuthenticationService } from 'src/app/services/authentication.service';@Injectable({providedIn: 'root'})export class AuthGuard implements CanActivate {constructor(private router: Router, private authenticationService: AuthenticationService) { }canActivate(next: ActivatedRouteSnapshot,state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {if(this.authenticationService.isLogin()){return true;}this.router.navigate(['/login']) ;return false;}}
  • จะเห็นว่า method isLogin เรายังไม่สร้างให้ไปที่ไฟล์ authentication.service.ts เพื่อสร้าง method isLogin ให้ return เป็น true ถ้ามีข้อมูล และถ้าไม่มีข้อมูลจะ return เป็น false
isLogin(): boolean{return !!this.token;}
  • เมื่อสร้าง Guard แล้วก็นำไปใช้ที่ไฟล์ app.module.ts ใน routes จะมีการเชคก่อนว่าถ้ามีการเข้า admin ต้อง login ก่อน จึงมาใส่ Guard ที่นี่
  • เมื่อเราลองเล่นใน Browser

6. ต่อมาเราจะไปดูเรื่อง unit test เราจะใช้คำสั่ง

ng test
  • จะเห็นว่าเมื่อเรารัน Unit Test ใน Project จะ Fail มหาศาลมาก เพราะว่าเราไม่ได้เขียน Test ตั้งแต่ครั้งแรกซึ่งเราจะกลับไปทำให้ Unit Test เขียวนั้นจะใช้เวลานานพอสมควรทางผู้สอนเลยให้สร้าง Project ใหม่เพราะว่าเวลาจะหมดล่ะ
  • มาสร้าง Project ใหม่กัน
ng new learnUnitTest
  • ทางผู้สอนจะใช้ case นับจำนวนตัวเลขที่ส่งเข้าไป
  • ให้ลองรัน ng test ที่โปรเจค learnUnitTest
  • จะเห็นว่า แดงตั้งแต่เริ่มเลย บางครั้งก็ งง กับ Angular CLI เหมือนกัน ว่าตอนที่สร้าง Project ชื่อ learnUnitTest อันที่จริงก็น่าจะ Welcome to learnUnitTest! แล้วทำไมยังจะให้ไป Welcome to app! อีก แต่ก็น่ะก็แก้ไขให้ตรงกับสิ่งที่เราคาดหวังกัน
  • ใน Test ที่ไม่ผ่านใน บรรทัดที่ 25 ของไฟล์ app.component.spec.ts ซึ่งใน Case นี้จะเขียนเชคว่าใน Tag ของ h1 จะต้องเจอคำว่า Welcome to learnUnitTest!
<h1>
Welcome to {{ title }}!
</h1>
  • แต่ใน Code ถ้าไปดูในไฟล์ app.component.html จะมีตัวแปรชื่อ title ซึ่งถูก set ค่าจาก app.component.ts ให้เป็นคำว่า app ดังนั้น h1 ดึงเป็นคำว่า Welcome to app!
  • ซึ่งถ้าเราไปอ่าน unit test อีก case จะเห็นสิ่งที่ expect อยากให้ title เป็นคำว่า app แต่เราอยากให้เป็นคำว่า learnUnitTest ไปที่ไฟล์ app.component.spec.ts ให้เปลี่ยนจาก app เป็น learnUnitTest
  • ดังนั้นถ้าเราเจอแบบนี้ เมื่อเราเขียน Unit Test สิ่งที่ควรระวังก็คือ จะต้องไปแก้ไขที่ Code หรือถ้าจะแก้ไขที่ unit test ถ้าไม่ใช้ unit test ที่ตัวเองเขียนก็ควรจะไป pair หรือไปถามคนที่อื่นไม่อย่างนั้นเราอาจจะเข้าใจผิดก็เป็นไปได้ ซึ่ง requirement อาจะเป็นแบบนั้นก็เป็นไปได้ และถ้าเราคิดว่าสิ่งที่เรา expect นั้นถูกตาม requirement ที่ได้รับ แสดงว่า code ที่เขียนมานั้นผิด ดังนั้นควรที่จะแก้ไขที่ code
  • ไปที่ไฟล์ app.component.ts ให้เปลี่ยนจาก app เป็น learnUnitTest
title = 'learnUnitTest';
  • ลองรัน ng test ใหม่ จะเห็นว่าผ่านแล้ว
  • ต่อมาเราจะไปเขียนลองเขียน unit test เพิ่มจำนวนกันในการลองเขียน unit test ครั้งนี้เราจะไปสร้าง service ขึ้นมาเพื่อเขียน unit test
ng g s add
  • เข้าไปที่ไฟล์ add.service.spec.ts (ให้ลบ Code อันที่ไม่ได้ใช้งานออกให้เหลือ และเพิ่มตามดังนี้)
import { AddService } from './add.service';describe('AddService', () => {
let service: AddService;
beforeEach(() => {
this.service = new AddService();
});
});
  • มาเพิ่ม case แรกกันเลย ใน case นี้จะบอกว่าให้ method value นั้น return 0 กลับมา
it('should initialize value to 0', () => {
expect(this.service.value()).toEqual(0);
});
  • เมื่อรัน test แล้วจะ Fail ก็จะแจ้ง Error บอกเราว่า this.service.value นั้นไม่ใช่ function เพราะว่าตอนนี้เรายังไม่มี method value() เลยย
  • เข้าไปที่ไฟล์ add.service.ts ให้ไปสร้าง method ชื่อ value() จะ return ค่าออกมาเป็น 0
  • ต่อไปเขียนโค้ตให้ผ่านโดยไม่ต้องคิดอะไรมาก เขียนไปในสิ่งที่เราคาดหวังไปเลยนั่นก็คือ 0
value(): number {return 0;}
  • เมื่อรัน Test แล้วจะผ่าน
  • มาเขียน Case ต่อไปกัน จะบอกว่า เมื่อส่งเลข 1 ไปที่ method add() จะ return 1 ออกมา ไปที่ไฟล์ add.service.spec.ts
it('should return 1 when add 1', () => {this.service.add(1);expect(this.service.value()).toEqual(1);});
  • เมื่อรัน Test แล้วไม่ผ่าน ก็แจ้ง error เหมือนเดิมก็คือ method add() นั้นไม่ใช่ function เพราะว่าตอนนี้เรายังไม่มี method add() เลยย
  • เข้าไปที่ไฟล์ add.service.ts ให้ไปสร้าง method ชื่อ add() จะทำการ set ค่าในตัวสักตัวแปร และเราใช้ method value() ดึง value มาเชคว่าเป็น 1
  • ต่อไปเขียนโค้ตให้ผ่านโดยไม่ต้องคิดอะไรมาก เขียนไปในสิ่งที่เราคาดหวังไปเลยนั่นก็คือ 1
import { Injectable } from '@angular/core';@Injectable({providedIn: 'root'})export class AddService {num: number;constructor() {this.num = 0;}value(): number {return this.num;}add(n: number){this.num = 1;}}
  • เมื่อรัน Test แล้วจะผ่าน
  • มาเขียน Case ต่อไปกัน เราจะเขียนว่า เมื่อส่งเลข 1 ไปสองรอบที่ method add() จะ return 2 ออกมา ไปที่ไฟล์ add.service.spec.ts
it('should return 2 when add 1 twice', () => {this.service.add(1);this.service.add(1);expect(this.service.value()).toEqual(2);});
  • เมื่อรัน Test แล้วไม่ผ่าน เพราะว่า Case ก่อนหน้าทาง Requiment บอกว่าอยากได้ Value เป็น 1 ครั้งนี้มี Requiment เพิ่มมาก็คืออยากได้ Value มีค่าเป็น 2
  • ต่อไปเขียนโค้ตให้ผ่านโดยไม่ต้องคิดอะไรมาก เขียนไปในสิ่งที่เราคาดหวังไปเลยนั่นก็คือ 2 เข้าไปที่ไฟล์ add.service.ts
  • ลองรัน Test ก็จะผ่านแล้ว
  • มาเขียน Case ต่อไปกัน เราจะเขียนว่า เมื่อส่งเลข 1 ไปสองรอบที่ method add() และส่งเลข 5 ที่ method add() อีกรอบ จะ return 7 ออกมา ไปที่ไฟล์ add.service.spec.ts
it('should return 7 when add 1 twice and add 5', () => {
this.service.add(1);
this.service.add(1);
this.service.add(5);
expect(this.service.value()).toEqual(7);
});
  • เมื่อรัน Test แล้วไม่ผ่าน เข้าไปที่ไฟล์ add.service.ts
  • ลองรัน Test ก็จะผ่านแล้ว จะเห็นว่าพอเริ่มมี case ใหม่เพิ่มเข้ามาก็จะทำให้ Logic ที่เราเขียนในโค้ตนั้นเราจำเป็นที่จะต้องปรับแล้ว ครั้งก่อนหน้าที่เรายังไม่ได้ปรับโค้ตให้ลองรับ case อื่นๆ ก็เพราะว่า เราก็ไม่รู้ว่าจะมี case อะไรบ้าง ซึ่งบางครั้งทางคนให้ requirement อาจจะบอกว่า ไม่คิดมั่งหรอว่าจะเกิด case นี้ แหมมมเราจะไปรู้ได้ยังไงล่ะค่ะ ว่าจะมี case ไหนบ้างที่จะเกิดขึ้น ซึ่งเซ้นของแต่ละคนก็ไม่เท่ากัน เราก็ควรเท่าที่เราทราบเท่านั้น
  • การเขียน unit test ไม่ได้ช่วยทำให้ Bug ไม่เกิดขึ้นเลย แต่ถ้าเราไม่เขียน case ที่ไม่คาดคิดก็อาจทำให้มี bug ขึ้นได้เหมือนกัน แต่ถ้าเรารู้ว่านี้คือ bug น่ะ ก็ควรที่จะเขียนคุมไว้ ครั้งต่อไปก็จะไม่ต้อง bug นี้เกิดขึ้นกับระบบของเรา
  • ในการเขียน Unit test ครั้งนี้จะเห็นว่าเราไม่ได้ Refactor code เลยเพราะเป็นตัวอย่างที่เรียบง่าย ซึ่งถ้าทำงานจริงๆ เมื่อเราเขียน Code ให้ผ่านแล้ว ก็ตรวจสอบดูสิว่า ควรที่จะ refactor code ในส่วนไหนได้บ้าง ซึ่งการที่เรา Refactor code แต่ละครั้งก็จะทำให้เรามั่นใจได้ว่าจะไม่ทำให้ case อื่นๆที่เราเขียนนั้นพัง เพราะว่าเรามี unit test แล้วว
  • การ Refactor Code คืออะไรล่ะ ก็คือ การที่เรากลับมาปรับปรุงโค้ดที่เคยเขียนไปแล้ว และทำงานได้อย่างถูกต้องแล้ว ให้มันสั้นลง กระชับมากขึ้น ทำงานได้เร็วมากขึ้น อ่านได้ง่ายมากขึ้น ลดความซับซ้อนของโค้ด

เย้ๆๆๆ จบแล้ววว ความจำฉันสั้น ขอจดไว้บันทึกความทรงจำตัวเอง ผิดพลาดประการใด ขออภัยมา ณ ที่นี้ด้วยย เขียนโค้ตมันก็สนุกดีน๊าา

สามารถเข้าไปดู source code angular ได้ที่

สามารถเข้าไปดู source code unit test ได้ที่

https://github.com/phatpan/learnUnitTest.git

--

--