กำหนดค่าเวอร์ชัน FVM พร้อมโปรเจ็กต์เพื่อความสอดคล้องกันระหว่างทีมและสภาพแวดล้อม CI
ตั้งค่าเวอร์ชัน flutter sdk global ในโปรเจ็กต์ต่างๆ
ติดตั้ง FVM
ติดตั้งผ่าน pub (package manager for the Dart programming language)
$ pub global activate fvm
*** Window OS ให้ทำการ set environment PATH \Users\poolsawat\AppData\Local\Pub\Cache\bin เพื่อให้สามารถเรียก fvm cli แบบ global ได้
fvm help เช็ค options ต่างๆ
$ fvm help
Flutter Version Management: A cli to manage Flutter SDK versions.
Usage: fvm <command> [arguments]
Global options:
-h, --help Print this usage information.
--verbose Print verbose output.
Available commands:
config Set configuration for FVM
flutter Proxies Flutter Commands
install Installs Flutter SDK Version
list Lists installed Flutter SDK Version
releases Lists Flutter SDK releases.
remove Removes Flutter SDK Version
use Which Flutter SDK Version you would like to use
version Prints the currently-installed version of FVM
Run "fvm help <command>" for more information about a command.
fvm list แสดงเวอร์ชัน flutter sdk ทั้งหมด
PS C:\WINDOWS\system32> fvm list
Versions path: \Users\poolsawat\fvm\versions
2.0.1
1.22.6
สั่งรันโค๊ดตัวอย่างที่ flutter มีให้ด้วยคำสั่ง “flutter run” หรือสั่ง run ผ่านไฟล์ lib/main.dart ด้วยปุ่ม Run
Flutter run key commands. r Hot reload. R Hot restart. h Repeat this help message. d Detach (terminate “flutter run” but leave application running). c Clear the screen q Quit (terminate the application on the device). An Observatory debugger and profiler on sdk gphone x86 arm is available at: http://127.0.0.1:53236/Vgi_F0zG2PU=/
function doGet(request) {
// sheet id
var ss = SpreadsheetApp.openById("ใส่ sheet id ของคุณ");
var sheet = ss.getActiveSheet()
const values = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()).getValues();
// Converts data rows in json format
const result = values.map(([a, b, c , d , e , f]) => {
return ({ id: a, topic: b, content: c, author : d , date : e , view : f})
})
return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);
}
openById : ให้ระบุ sheet id ที่ได้จาก https://docs.google.com/spreadsheets/d/187hL4DXXXXXXXXXXXXXXXXXXXXXXX-p_JOGjYxU/edit#gid=0 ซึ่งของแต่ละ sheet จะไม่ซ้ำกัน
CREATE TABLE `photos` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`photo_origin_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`photo_new_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`photo_temp_path` varchar(255) NOT NULL,
`photo_extension` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`photo_status` enum('active','inactive') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`photo_date` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
สร้าง Class Model Photo (Photo.php)
php artisan make:Model Photo
แก้ไขไฟล์ app\Models\Photo.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Photo extends Model
{
use HasFactory;
protected $table = 'photos';
protected $fillable = [
'id', 'photo_origin_name', 'photo_new_name',
'photo_extension', 'photo_status', 'photo_date', 'photo_temp_path'
];
public $timestamps = false;
}
สร้าง Class Controller UploadFileController (UploadFileController.php)
<?php
namespace App\Http\Controllers;
use App\Models\Photo;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class UploadFileController extends Controller
{
public function simpleUplaod(Request $request)
{
try {
if ($request->file('photo')->isValid()) {
$path = $request->photo->path();
$extension = $request->photo->extension();
$clientOriginalName = $request->photo->getClientOriginalName();
$newFileName = time() . $clientOriginalName;
$uploadedFile = $request->file('photo');
// Save File to local drive
Storage::putFileAs('photos', $uploadedFile, $newFileName);
//Save File to Photo table
$photo = new Photo();
$photo->photo_temp_path = $path;
$photo->photo_origin_name = $clientOriginalName;
$photo->photo_new_name = $newFileName;
$photo->photo_extension = $extension;
$photo->photo_status = 'ACTIVE';
$photo->photo_date = Carbon::now();
$photo->save();
return [
'path' => $path,
'extension' => $extension,
'clientOriginalName' => $clientOriginalName,
'newFileName' => $newFileName
];
}
} catch (\Throwable $th) {
return $th->getMessage();
}
}
}
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $addHttpCookie = true;
protected $except = [
'upload'
];
}
$composer require tymon/jwt-auth
Using version ^1.0 for tymon/jwt-auth
./composer.json has been updated
Running composer update tymon/jwt-auth
Loading composer repositories with package information
Updating dependencies
...
[32mPackage manifest generated successfully.[39m
2 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
$php artisan jwt:secret
jwt-auth secret [Wwt4F********************************************UNvQ14f] set successfully.
4. แก้ไข class User.php เพิ่ม implement JWTSubject
<?php
namespace App;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements JWTSubject
{
use Notifiable;
// Rest omitted for brevity
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
การพัฒนาแอพพลิเคชั่น ข้อแนะนำคือควรทำ Unit Tests มองการพัฒนาในระยะยาว หากต้องมีการแก้ไข issue เล็กน้อย แต่ไม่แน่ใจว่าจะเกิด defect กับระบบเดิมหรือไม่ การมี unit tests ช่วยประหยัดเวลาการ regression tests ได้เยอะมาก และคุณจะเห็นประโยชน์ของการทำ Unit tests อย่างแน่นอน
รู้จัก @RestController annotation สร้าง routes RESTful API
annotation @RestController implement มาจาก @Controller ของ spring web เพื่อใช้งานสำหรับการสร้าง RESTful API (ไม่ใช่ static view) เพิ่มความสะดวกในการพัฒนา RESTful API ให้ง่ายยิ่งขึ้น
ตัวอย่างการสร้าง Route (text/plain)
package com.poolsawat.starter.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot!";
}
}
ทดสอบ http://localhost:8080/
Greetings from Spring Boot!
เพิ่ม Route (application/json)
...
@RequestMapping("/domain")
public String[] json() {
return new String[] {"www",".","poolsawat",".","com"};
}
...
restart server จากนั้น ทดสอบ http://localhost:8080/domain
[
"www",
".",
"poolsawat",
".",
"com"
]
เพิ่ม Test Dependencies ใน POM
ก่อนที่จะเริ่มเขียน Unit Tests จำเป็นต้องเพิ่ม “spring-boot-starter-test” เข้าไปที่ไฟล์ POM (ไฟล์ pom.xml) มาเพิ่มกันเลย
.andExpect(status().isOk()) expect htto status code OK (status code 200) หากไม่ใช่จะ fail ทันที
.andExpect(content().string(equalTo(“Greetings from Spring Boot!”))) expect content body ต้องเป็นคำว่า “Greetings from Spring Boot!” หากไม่ใช่จะ fail ทันที
จากนั้นสร้าง maven test build
apply -> run
ตรวจสอบที่ console panel
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.poolsawat.starter.controller.HelloControllerTest
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.606 s - in com.poolsawat.starter.controller.HelloControllerTest
2020-10-15 10:57:54.262 INFO 5864 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.231 s
[INFO] Finished at: 2020-10-15T10:57:54+07:00
[INFO] Final Memory: 18M/220M
[INFO] ------------------------------------------------------------------------
ในทุกครั้งที่มีการแก้ไข java code จำเป็นต้องมีการ restart web server spring boot มี hot reload (การ auto restart web server เมื่อมี code change)
การพัฒนาแอพพลิเคชั่นที่มี Test ถือว่าเป็นแอพพลิเคชั่นที่ดี ข้อดีของการทำ Test มีเยอะมากช่วยลดเวลาการ Regression Test ได้เยอะ ถ้าแอพพลิเคชั่นของคุณยังไม่มี test เริ่มทำได้แล้ว เพราะถ้าไม่ทำคุณจะคุยกับเขา (ทีมพัฒนา แอพพลิเคชั่นที่มี test) ไม่รู้เรื่อง บทความต่อไปจะเป็นเรื่องเกี่ยวกับอะไร คอยติดตามกันนะครับ
laravel สร้าง generate /app/Http/Controllers/PhotoController.php ภายในไฟล์จะมีการสร้าง function RESTful template มาให้
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PhotoController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}
มาทำความรู้จักการทำงานแต่ละ function มามีการทำงานอย่างไร ตามการอธิบายจากตารางนี้
implement PhotoController ตาม function การทำงานเพื่อใช้ตรวจสอบการ call จาก REST client
ผมจะเพิ่ม function response()->json(); เพื่อทำการ return http response คืนค่ากลับไปในรูปแบบ application/json format
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PhotoController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return response()->json(['name' => 'index']);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return response()->json(['name' => 'create']);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
return response()->json(['name' => 'store', 'payload' => $request->all()]);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
return response()->json(['name' => 'show', 'id' => $id]);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
return response()->json(['name' => 'edit', 'id' => $id]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
return response()->json(['name' => 'update', 'payload' => $request->all(), 'id' => $id]);
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
return response()->json(['name' => 'destroy', 'id' => $id]);
}
}
visit ใช้สำหรับเข้า website test ถือว่าเป็น command ที่ต้องรู้ลำดับแรก ๆ ตัวอย่างการใช้งาน cy.visit(‘https://google.com‘) มี function callback ที่น่าสนใจ คือ onBeforeLoad , onLoad สามารถใส่ behavior function การทำงานในส่วนนี้ได้
get ระบุ selector element ของหน้าจอ เพื่อจะเป็น target ในการ action command ถัดไปกรณี ต้องการให้ระบุ element เพื่อทำงานอะไรบ้างอย่าง เช่น set value ในช่อง text ,set text บน label เป็นต้น ตัวอย่างการเรียกใช้งาน cy.get(‘{selector}’) โดย selector คือ input ,.class ,#id ,[name=””] เป็นต้น
type ใช้สำหรับ set value ให้กับ input tag ต่าง ๆ เช่น input [text ,date ,phone ,password ,number ,…] รวมถึง textarea ได้อีกด้วย ตัวอย่างการใช้งาน cy.get(‘{selector}’).type(‘commade type’)
clear ใช้ clear value ในช่อง input ตัวอย่างการใช้งาน cy.clear()
as ทำหน้าที่เป็น command กำหนด alias เป็นการกำหนด reference ให้กับ command ที่กำลังสนใจ หรือ ใช้งานซ้ำ ๆ สามารถ reuse เรียกใช้งานซ้ำได้ ตัวอย่างการใช้งาน cy.(‘{selector}’).as(‘mySelector’) อยากต้องการระบุอ้างอิงถึงก็เพียงแค่ cy.get(‘@mySelector’) ถึงชื่อ alias ก็สามารถใช้ทำงานได้เหมือนการเรียก cy.get(‘{selector}’) นั้นอีกครั้ง
check ใช้กับ input [radio ,checkbox] กำหนด state ให้กับ element เหล่านี้เป็นการ checked ตัวอย่างการใช้งาน cy.get(‘{selector}’).check() โดยที่ {selector} นั้นต้องเป็น radio ,checkbox command ถึงจะทำงานได้ถูกต้อง
click ชื่อก็บอกอยู่แล้ว กำหนด event click ให้กับ element ที่กำลังสนใจ เกือบแทบทุก element สามารถเรียก command นี้ได้เกือบหมด (ถ้าไม่ถูก Disabled) ตัวอย่างการใช้งานจะใช้งานร่วมกับ command get คือ cy.get(‘{selector}’).click()
contains สำหรับเทียบค่าข้อความที่คาดหวังว่าจะมีใน element ที่เรากำลังสนใจ (คล้ายกับการ LIKE ‘%%’) ตัวอย่างการใช้งาน cy.get(‘{selector}’).contains(‘ข้อความ’) ถ้าหา element ที่มีข้อความไม่พบจะตก fail case เลย
each การ LOOP elements ที่ได้จากการ get(‘{selector}’) แล้วต้องการที่จะทำงานอะไรบ้างอย่างกับ element ที่ selector ได้ ก็จะเรียก command each นี้ โดย command จะมี callback function ให้เขียนคำสั่งอื่นได้ ๆ ตัวอย่างการใช้งาน cy.get(‘{selector}’).each(()=> {/* todo something */ })
eq ตัวย่อของ equal ที่แปลว่า เท่ากัน case ที่ใช้งานบ่อย คือการเทียบหาลำดับของ elements ที่พบได้มากกว่า 1 element ตัวอย่างการใช้งาน cy.get(‘{selector}’).eq(0) โดย 0 คือ index ของ elements ที่เจอ
find ต้องการค้นหา element เป้าหมายเพียง element เดียว หลังจากที่ selector ได้ elements มากกว่า 1 หรือจะระบุ selector ตั้งแต่แรกเริ่ม get เลยก็ได้ ตัวอย่างการใช้งาน cy.get(‘{selector}’).find(‘.class-unique’) command จะ return element มาเพียง 1 element เท่านั้น
log แสดง variable หรือ text ออกทางหน้าจอ run ui คล้ายกับ console.log(”) ของ javascript เพื่อแต่พื้นที่การแสดงอยู่คนละที่กัน
next ถัดไป จะ selector element ตำแหน่งถัดไปของ element ก่อนเรียก command นี้ โดยสังเกตุจะเป็น element level เดียวกับ element ที่เรียก command นี้ ตัวอย่างการใช้งาน cy.get(‘{selector}’).next() จะได้ element ถัดไปทันที
prev ก่อนหน้า เมื่อมี element ถัดไป (next command) ก็ต้องมีการหา element ก่อนหน้า ดังนั้น command นี้จะทำหน้าที่หา element ก่อนหน้าที่จะเรียก command นี้
not เป็นนิเสธน์ ใช้กรอง element ไม่สนใจ elements ที่ถูกเรียกภายใน command นี้ ตัวอย่างการเรียกใช้งาน cy.get(‘{selectors}’).not(‘{.not-use-element}’)