Category: Developer

งานพัฒนาระบบ, เขียนโปรแกรม

  • django (ดี)จังโก้ ดีอย่างไร #01

    ขอไม่ลงรายละเอียดว่า อะไรคือ Web Framework, MVC, MVT พวกนั้นนะครับ อ่านเองที่ https://www.djangoproject.com/ และ เขียนวิธีติดตั้งบน Windows ไว้คร่าว ๆ ที่ ขั้นตอนการติดตั้ง django บน Windows และในที่นี้ใช้ Visual Studio Code เป็น Editor (สั่งด้วยคำสั่ง code …)

    โจทย์

    สมมุติในทีม มีคน 10 คน ต้องการ ระบบบันทึกการปฏิบัติงาน

    1. จัดเก็บข้อูล วันเวลาของงานที่ทำ, ประเภทของงงาน (ตาม TOR), รายละเอียดของงานที่ทำ, ระยะเวลาที่ใช้ไป (ชั่วโมง)
    2. แต่ละคน ต้อง Login เข้ามาก่อน จึงจะบันทึกปฏิบัติงานได้

    วิถีแบบ django

    สร้าง project ชื่อ myproject

    django-admin startproject myproject

    สร้าง App ชื่อ worklog

    cd myproject
    python manage.py startapp worklog

    สร้าง Model ว่าจะเก็บข้อมูลอะไรบ้าง

    แก้ไขไฟล์

    code worklog/models.py

    แล้วสร้าง Class ชื่อ Worklog เพื่อกำหนด Field เป็นช่องทางการเก็บค่าตามโจทย์ (Reference: https://docs.djangoproject.com/en/2.1/ref/models/fields/)
    –> ขั้นตอนนี้ เขียนใน Visual Studio Code เสร็จแล้ว Save and Exit

    from django.db import models
    from django.utils.timezone import now
    
    # Create your models here.
    class Worklog(models.Model):
        timestamp = models.DateTimeField(default = now())
        typeOfWork = models.ForeignKey('TypeOfWork', on_delete=models.CASCADE)
        work_text = models.TextField(default="")
        manhour = models.FloatField(default=0)
        def __str__(self):
            return self.work_text
    
    class TypeOfWork(models.Model):
        typeOfWork_text = models.CharField(max_length=200)
        def __str__(self):
            return self.typeOfWork_text

    บอกให้ django สร้างโครงสร้างฐานข้อมูลตามโมเดล (จาก Command Prompt)

    python manage.py migrate

    เพิ่ม App ‘worklog’ เข้าสู่ myproject

    แก้ไขไฟล์

    code myproject/settings.py

    เพิ่ม ‘worklog’ ในส่วนของ INSTALLED_APPS
    –> ขั้นตอนนี้ เขียนใน Visual Studio Code เสร็จแล้ว Save and Exit

    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'worklog'
    ]

    สร้าง Super User สักคนนึง

    python manage.py createsuperuser

    ตั้งชื่อว่า admin และ รหัสผ่านตามต้องการ

    เพิ่มโมเดล TypeOfWork และ WorkLog เข้าสู่หน้าของ Admin โดยการแก้ไขไฟล์
    –> ขั้นตอนนี้ เขียนใน Visual Studio Code เสร็จแล้ว Save and Exit

    code worklog/admin.py

    ดังนี้

    from django.contrib import admin
    
    # Register your models here.
    from .models import TypeOfWork, Worklog
    admin.site.register(TypeOfWork)
    admin.site.register(Worklog)

    สั่ง Run Server

    สั่งที่ Command Prompt

    python manage.py runserver

    แล้วเปิด Web Browser ไปที่
    http://127.0.0.1:8000/admin/

    ใส่ Login และ Password ของ admin ทั้งตั้งไว้ก่อนหน้านี้

    เริ่มต้นใช้งาน

    เพิ่มประเภทของงาน

    คลิก Type of works — django พยายามใส่ s ให้ด้วยอัตโนมัติ
    คลิกที่ปุ่ม ADD TYPE OF WORK
    เพิ่มประเภทของงาน วนไป
    เสร็จแล้วได้ผลประมาณนี้ อยากจะ Edit Delete ได้หมด

    เพิ่มบันทึกการปฏิบัติงาน

    คลิกที่ Add ในส่วนของ Worklogs

    มี Form สำหรับ Input ทันที

    สวยงาม ไม่ต้องทำอะไรเพิ่ม เลือก Type of works ได้ ช่องวันที่ เวลา ก็มี Widget ให้เรียบร้อย
    แก้ไขไป มี History ให้ด้วย

    จากนั้น ก็เพิ่มคนเข้าทีม ด้วยเมนู Users ได้

    ระบบ Security พร้อม

    User01 ตั้งค่าให้เป็น Worklog > Can Add Worklog
    ก็จะทำได้แค่เข้ามาบันทึกปฏิบัติงานเท่านั้น

    สรุป

    จะเห็นได้ว่า ด้วยการสร้างโมเดลเล็กน้อย django ก็สามารถสร้างระบบ User Entry ง่าย ๆ ที่มาพร้อม Security Features มากมายได้แล้ว ยังมีรายละเอียดอีกเยอะ โดยเฉพาะในส่วนของ View/Template ที่จะสร้าง User Input และการออกรายงานต่าง ๆ รวมถึง การสร้าง API และ RESTful API หรือ จะผูกกับ OAuth2 ก็ยังได้

    หวังว่าเป็นประโยชน์ครับ

  • ปัญหาการ Debug บางคำสั่งใน .NET Web Project (VS.NET 2013)

    ในการพัฒนา web application ด้วย Visual Studio.NET 2013 ผู้พัฒนาบางท่าน อาจจะเคยเจอปัญหาเมื่อ debug ไปถึงบางคำสั่งแล้ว คำสั่งไม่ทำงานตามที่ควรจะเป็น หรือบางครั้งก็ไม่มีการ return ค่าออกมา ทั้ง ๆ ที่ค่าของข้อมูลถูกต้อง ซึ่งหากเจอปัญหานี้แล้ว ให้ลองสังเกตดังต่อไปนี้

    1. เครื่องที่ใช้พัฒนาเป็น Windows 32 bit หรือ 64 bit
    2. ตรวจสอบแน่ใจแล้วว่าใช้งานคำสั่งได้ถูกต้อง และข้อมูลที่คำสั่งนี้นำไปประมวลผลมีความถูกต้องแล้ว

    ถ้าเครื่องที่ใช้เป็น Windows 64 bit ให้ทดลองทำตามดังนี้ คือ

    1. ไปที่เมนู TOOLS –> Options
    2. จะปรากฏหน้าต่าง Options
    3. เมนูด้านซ้ายมือของหน้าต่าง Options เลือกเมนูย่อย Projects and Solutions และภายใต้เมนูนี้ เลือกเมนูย่อย Web Projects จะเห็นตัวเลือกด้านขวามือสองตัวเลือก ให้เลือกเครื่องหมายถูกตัวเลือกแรก คือ Use the 64 version…
    4. คลิกปุ่ OK และทดลอง run project และ debug คำสั่งที่พบปัญหาอีกครั้ง

     

    ตัวอย่างคำสั่งที่พบปัญหา

    สำหรับตัวอย่างคำสั่งที่เคยพบปัญหาเมื่อทำการ debug คือ คำสั่งที่ใช้ในการอ่านข้อมูลจาก registry

    Registry.LocalMachine.OpenSubKey

    โดยคำสั่งนี้จะอ่านข้อมูลจาก registry ตาม key ที่ระบุ  โดยรายละเอียดปัญหาที่พบคือ มีข้อมูลใน registry ตาม key ที่ระบุจริง แต่ตอน debug กลับ return ค่าว่างกลับมา  ซึ่งหากไปใช้ใน projects ที่เป็น Windows และทำการ debug คำสั่งนี้จะ return ข้อมูลออกมาได้ถูกต้อง

  • การเรียกใช้งาน constructor จากอีก constructor ใน C#.NET

    constructor คืออะไร

    constructor ก็คือ ฟังก์ชันที่ถูกเรียกใช้งาน เมื่อ object ถูกสร้างขึ้นมา ซึ่งใน C#.NET  ก็คือฟังก์ชันที่ตั้งชื่อเป็นชื่อเดียวกับชื่อ class นั่นเอง

     

    constructor มีประโยชน์อย่างไร

    ในการสร้าง object ขึ้นมา บางครั้งต้องมีการระบุค่าเริ่มต้นบางอย่างให้กับ object นั้น ซึ่ง constructor ก็จะเป็นเครื่องมือหนึ่งที่เป็นตัวช่วยบังคับให้ต้องระบุค่าเริ่มต้นนี้ตอนสร้าง object นั่นเอง

     

    ยกตัวอย่าง เช่น

    class Student

    {

    public Student(string studentID)

    {

    คำสั่งดึงข้อมูลนักศึกษาด้วย Student ID โดยใช้ข้อมูล Student ID จาก argument studentID

    }

    }

    จากตัวอย่างนี้ ต้องการสร้าง object Student โดยใช้ Student ID เพื่อไปดึงข้อมูลเบื้องต้นของนักศึกษามาแสดง

    ดังนั้น object ใหม่ที่สร้างจาก class Student ก็จะต้องระบุค่า Student ID ให้กับ argument studentID ก่อน

    ตัวอย่างการเรียกใช้งานคือ

    Student student = new Student(12345);

    student ก็คือ object ที่ถูกสร้าง

    123456 คือ ข้อมูล Student ID ทีต้องระบุตอนที่ object นี้ถูกสร้างนั่นเอง

     

    ทำไมต้องเรียกใช้ constructor จากอีก constructor หนึ่ง

    ในบางครั้งเมื่อเราใช้งาน class ไประยะเวลาหนึ่ง อาจะเจอกรณีที่ constructor เดิม ยังไม่รองรับความต้องการที่เปลี่ยนไปหรือเพิ่มขึ้น   แต่ความต้องการใหม่นั้นยังคงต้องใช้ชุดคำสั่งและขั้นตอนการทำงานเหมือนกับที่มีอยู่เดิมด้วย และมีการเพิ่มเติมชุดคำสั่งใหม่เข้าไปเล็กน้อยเพื่อให้รองรับกับความต้องการใหม่ด้วย ซึ่งทางเลือกที่เป็นไปได้คือ

    1. ปรับแก้ constructor เดิมที่มีอยู่ให้รองรับความต้องการใหม่
    2. สร้าง constructor ใหม่ โดยทำการสำเนาชุดคำสั่งทั้งหมดใน constructor เดิมมา
    3. สร้าง constructor ใหม่ โดยเรียกใช้งาน constructor เดิม

    มาพิจารณาแต่ละทางเลือก

    ทางเลือกที่ 1 เป็นการปรับแก้ของเดิมที่มีอยู่แล้ว ซึ่งเป็นทางที่ควรเลี่ยงที่สุด เพราะอาจจะไปกระทบกับการทำงานเดิมที่ถูกต้องอยู่แล้ว และอาจจะกระทบเป็นลูกโซ่กับ object อื่นที่มาเรียกใช้

    ทางเลือกที่ 2 สำเนาคำสั่งทั้งหมดจาก constructor เดิม มาใส่ใน constructor ใหม่ แล้วเขียนคำสั่งเพิ่มเพื่อให้รองรับความต้องการใหม่ ซึ่งทางเลือกนี้ถือว่าดีกว่าทางเลือกแรก เพราะไม่กระทบกับการทำงานเดิมแน่นอน แต่จะเห็นว่าเกิดความซ้ำซ้อนกันของชุดคำสั่งของ constructor เดิม และใหม่ และจะพบปัญหาในกรณีที่มีความจำเป็นต้องปรับปรุงคำสั่งที่ใช้อยู่ใน constructor ทั้งสอง ผู้พัฒนาก็จะต้องมาแก้ทั้งสองที่ให้เหมือนกันอีก ซึ่งจะเป็นการเสียเวลา

    ทางเลือกที่ 3 เรียกใช้งาน constructor เดิม ซึ่งจะเห็นว่าเป็นวิธีการที่สะดวกรวดเร็วที่สุด หากต้องการปรับปรุงคำสั่ง ก็เพียงไปจัดการที่ constructor เดิมเท่านั้น

     

    วิธีการเรียกใช้งาน constructor เดิม จาก constructor ใหม่

    syntax ที่ใช้ใน C#.NET คือ  หลังจากส่วนประกาศ constructor ให้ใช้  : this() เพิ่มเข้าไป

    ยกตัวอย่าง

     

    class Student

    {

    public Student(string studentID) //constructor เดิม

    {

    คำสั่งดึงข้อมูลนักศึกษาด้วย Student ID โดยใช้ข้อมูล Student ID จาก argument studentID

    }

     

    public Student(string studentID, string citizenID) : this(studentID)  //constructor ใหม่

    {

    คำสั่งใหม่ที่เพิ่มเติม

    }

    }

  • Puppeteer ควบคุมและบันทึกการใช้งาน chrome สำหรับ UI testing

    Puppeteer เป็น Node library ที่มีชุดของ API สำหรับการควบคุม Google Chrome หรือ Chromium ผ่าน DevTool protocol ทำงานในรูปแบบ headless โดย default แต่สามารถกำหนดให้ทำงานแบบ full (non-headless) Chrome ได้  ที่สำคัญไม่ต้องทำงานผ่าน Web Driver อีกต่อไป

    puppeteer สามารถทำงานได้ทุกอย่างที่สามารถทำได้โดย manual บน browser เช่น

    • สร้าง screenshots และ PDFs ของ page
    • Automate form submission
    • UI testing
    • keyboard input

    การติดตั้ง Puppeteer

    การติดตั้ง Puppeteer เพื่อใช้งานใน project สามารถทำได้ผ่านทาง NPM โดยใช้คำสั่ง

    npm i puppeteer

    เมื่อทำการติดตั้ง Puppeteer จะทำการ downloads Chromium version ล่าสุดซึ่งสามารถทำงานร่วมกับ api ได้อย่างสมบูรณ์ (ประมาณ 170 Mb สำหรับ Mac, 282 Mb สำหรับ Linux และ 280 Mb สำหรับ Windows) ถ้าไม่ต้องการ download Chromium ในระหว่างการติดตั้ง สามารถยกเลิกได้โดยการกำหนดค่า “PUPPETEER_SKIP_CHROMIUM_DOWNLOAD”  environment variables

    การใช้งาน Puppeteer

    Puppeteer API ให้ความสามารถในการสร้าง instance ของ  browser, เปิด webpage และบันทึก screenshot โดยสร้าง file ชื่อ example.js และเขียน code ดังนี้

    เรียกใช้งาน script  บน command line โดยใช้คำสั่ง

    node example.js

    Puppeteer กำหนด default ขนาดของ page ที่ 800 x 600px  และสามารถเปลี่ยน page size โดยใช้ page.setViewport() ซึ่งสามารถศึกษาเพิ่มเติมการใช้งาน Puppeteer API ได้ที่ https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md

    ผลของการ run script  จะทำการบันทึก screenshot ของ https://example.com ไปที่ file “example.png”

    จะเห็นว่าในระหว่างการ run script ไม่ได้ปรากฎ browser ขึ้นมาให้เห็น เนื่องจากโดยค่า default จะทำงานเป็น headless mode แต่สามารถเปลี่ยนการทำงานได้โดยกำหนด headless = false หากต้องการดูการทำงาน

    const browser = await puppeteer.launch({
    	             headless: true
    	         });  // default is true

     

    อ้างอิง

    https://github.com/GoogleChrome/puppeteer/

  • Visual test automation : Appraise test page

    จากบทความ Visual test automation ที่ได้กล่าวถึง Appraise ที่ใช้สำหรับทำการทดสอบการแสดงผลแบบอัตโนมัติในเบื้องต้น ตั้งแต่การติดตั้ง ตัวอย่าง test page, test fixture การเรียกใช้งานการทดสอบ และผลการทดสอบ บทความนี้จะมาลงรายละเอียดในการสร้าง test page

    Creating test pages

    Appraise สามารถกำหนดรูปแบบข้อกำหนดการทดสอบสำหรับการทดสอบส่วนแสดงผลได้ไม่ยุ่งยาก โดย Appraise จะทำการอ่านข้อมูลนำเข้า และ ผลลัพธ์ที่คาดหวัง จาก file ที่เขียนในรูปแบบ Markdown แล้วส่งต่อให้กับระบบทำการทดสอบ จากนั้นจะบันทึกภาพการแสดงผลที่เกิดขึ้น เปรียบเทียบภาพการแสดงผลที่ได้จริงกับผลลัพธ์ที่คาดหวัง โดยมีสิ่งที่จำเป็นที่จะต้องกำหนดสำหรับการทดสอบ 3 ส่วน คือ

    1. input
    2. expect output
    3. fixture

    Input (parameters ของ example)

    input parameters อยู่ในรูปแบบ text JSON หรือ YAML  ในการกำนด input parameters สำหรับแต่ละการทดสอบหรือ example จะต้องกำหนด block ของ example และกำหนดชื่อให้กับ example ในส่วนเริ่มต้น block ในรูปแบบ example=”ชื่อ”

    จากรูปข้างต้น จะเห็นว่ามี example ชื่อ “first” ถูกกำหนดรูปแบบเป็น YAML ซึ่ง Appraise จะรู้ว่าจะนำข้อมูลไปได้อย่างไร ในแต่ละ test page สามารถมี eaxmple ได้มากกว่าหนึ่ง example โดยที่แต่ละ example จะต้องมีชื่อที่ไม่ซ้ำกัน ซึ่งจำเป็นในการใช้สำหรับเปรียบเทียบกับผลลัพธ์ที่คาดหวังด้วย

    Expect output

    ecpect output – ผลลัพธ์ที่คาดหวัง ซึ่งอยู่ในรูปแบบ file รูปภาพ .png  การกำหนดผลลัพธ์ที่คาดหวังใน test page ทำได้โดยระบุ

    ![ชื่อ example](ชื่อ file รูปภาพ .png ที่เป็นผลลัพธ์ที่คาดหวัง)

    ซึ่งจะอยู่ส่วนใดๆใน test page ก็ได้ ไม่ว่าจะอยู่ก่อน example input ก็ได้ โดยที่ความเชื่อมโยงระหว่าง input กับ expect output ก็คือชื่อของ example ที่กำหนดใน []

    ในการสร้าง test page อาจจะไม่ต้องกำหนด expect output ก่อนก็ได้เช่นกัน เมื่อทำการ run test ครั้งแรก ผลที่ออกมาจะ failed เนื่องจากไม่ได้ระบุ expect output แต่สามารถที่จะบันทึกผลที่ได้จากการ run test ใช้เป็น expect output สำหรับการ run test ครั้งต่อไปได้

    Fixture

    fixture คือส่วนของ code ที่ Apprise ใช้ในการเชื่อมต่อกับระบบที่จะทดสอบ รวมทั้งกำหนดการใช้งานและการประมวลผล input ในการทดสอบ โดยทั่วไป examples ที่เชื่อมโยงกันจะใช้ fixture เดียวกัน

    การกำหนด fixture ทำโดยการระบุ fixture =”ชื่อ fixture” ในส่วนของ header ของ example block

    จากรูปข้างต้น จะเห็นว่ามี example ชื่อ “first” มีการกำหนด fixture คือ “hello.js”

    จากตัวอย่าง fixture ด้านบน เป็น fixture ที่มีการสร้าง output page ออกมาโดยตรงโดยไม่ได้เชื่อมต่อกับระบบใดๆ ซึ่งในการใช้งานจริง จะต้องเชื่อมต่อกับระบบหรือส่วนที่จะทำการทดสอบจริงที่ทำการ render ผลลัพธ์ออกมา

    • input parameters จาก test example ส่งผ่านมายัง fixture ผ่านทาง argument ตัวแรก
    • fixture return object ซึ่งมี properties 2 ตัวคือ contentType และ content อย่างไรก็ตาม fixture ยังสามารถ return ข้อมูลแบบอื่นได้เช่นกัน เช่น remote URL หรือ file ใน temporary folder ก็ได้เช่นกัน
    • contentType จาก fixture ด้านบนคือ text/html  ซึ่งสามารถเป็น contenType อื่นใดก็ได้ที่ browser สามารถ render ได้ รวมทั้ง SVG, PDF หรือ image

     

    อ้างอิง 

    https://github.com/AppraiseQA/appraise
    https://github.com/AppraiseQA/appraise/blob/master/examples/creating-test-pages.md

  • Stencil : Decorators

    Component Decorator

    แต่ละ Stencil component จะต้องขึ้นต้นด้วย @Component() decorator เสมอ โดย import มาจาก @stencil.core package ซึ่งภายใต้ @Component() decorator สามารถกำหนด tag name และ styleUrl ของ component

    import { Component } from '@stencil/core';
    
    @Component({
      tag: 'todo-list',
      styleUrl: 'todo-list.scss'
    })
    export class TodoList {
      ...
    }
    

    @Component() decorator ให้ความสามารถในการกำหนด CSS classes และ attributes บน componnet ที่สร้างโดยใช้ host option ดังนี้

    import { Component } from '@stencil/core';
    
    @Component({
      tag: 'todo-list',
      styleUrl: 'todo-list.scss',
      host: {
        theme: 'todo',
        role: 'list'
      }
    })
    

    เมื่อใช้งาน component ตัวนี้ ก็จะมีการกำหนดค่า class เป็น todo และกำหนด role attribute ให้อัตโนมัติ

    <todo-list class='todo' role='list'></todo-list>

     

    Prop Decorator

    @Prop() decorator ใช้ระบุ attribute หรือ properties สำหรับ element ที่ผู้พัฒนาสามารถกำหนดค่าให้กับ component นั้นได้  เนื่องจาก Children component จะไม่สามารถเข้าถึง properties หรือ reference ของ parent component ได้จึงใช้ Props ในการผ่านข้อมูลจาก parent มาสู่ child และเมื่อใดก็ตามที่ค่าของ Props เปลี่ยนแปลงจะทำให้ component ทำการ re-render

    type ของ Props ที่รองรับมีหลากหลาย ไม่ว่าจะเป็น number, string, boolean หรือแม้กระทั่ง Object หรือ Array

    import { Prop } from '@stencil/core';
    ...
    export class TodoList {
      @Prop() color: string;
      @Prop() favoriteNumber: number;
      @Prop() isSelected: boolean;
      @Prop() myHttpService: MyHttpService;
    }
    

    ภายใน function ใน TodoList class สามารถเรียกใช้งาน Props ผ่าน this operator

    logColor() {
      console.log(this.color)
    }
    

    สำหรับการใช้ภายนอก ใน HTML การกำหนดค่าทำได้โดย set ค่าให้ attributes ของ component tag โดยใช้ dash-case  เช่น Prop favoriteNumber ใช้ attribute favorite-number

    <todo-list color="blue" favorite-number="24" is-selected="true"></todo-list>

    สำหรับการใช้ใน JSX การกำหนดค่าทำได้โดย set ค่าให้ attributes ของ component tag โดยใช้ camelCase

    <todo-list color="blue" favoriteNumber="24" isSelected="true"></todo-list>

    Prop สามารถเรียกใช้งานผ่านทาง element ของ  javascript

    const todoListElement = document.querySelector('todo-list');
    console.log(todoListElement.myHttpService); // MyHttpService
    console.log(todoListElement.color); // blue
    

     

    Component State

    @State() decorator ใช้ในการจัดการ internal data ของ component ซึ่งผู้ใช้ไม่สามารถแก้ไขข้อมูลจากภายนอก component  เมื่อข้อมูลมีการเปลี่ยนแปลง จะทำให้ component ทำการ re-render เช่นเดียวกับ Prop

    import { State } from '@stencil/core';
    
    ...
    export class TodoList {
      @State() completedTodos: Todo[];
    
      completeTodo(todo: Todo) {
        // This will cause our render function to be called again
        this.completedTodos = [...this.completedTodos, todo];
      }
    
      render() {
        //
      }
    }
    

     

    Element Decorator

    @Element() decorator ใช้ในการเข้าถึง host element ภายใน class instance โดย return type คือ HTMLElement  ทำให้สามารถใช้ standard DOM methods/events ได้

    import { Element } from '@stencil/core';
    
    ...
    export class TodoList {
    
      @Element() todoListEl: HTMLElement;
    
      addClass(){
        this.todoListEl.classList.add('active');
      }
    }
    

     

    อ้างอิง : https://stenciljs.com/docs/decorators

  • Visual test automation

    ในการพัฒนา software นั้น เรื่อง Look and Feel เป็นสิ่งที่สำคัญมาก ๆ software ที่ทำงานได้ดียังไม่พอ ต้องดูดีสวยงามและใช้งานง่ายอีกด้วย คำถามที่น่าสนใจคือ จะทำการทดสอบในส่วนของการแสดงผลอย่างไรบ้าง

    โดยปกติในการทำการทดสอบการแสดงผล จะใช้คนเป็นผู้ทดสอบ มานั่งดู มาใช้งาน แล้วพิจารณาและตัดสินว่า ถูกหรือไม่ ทำให้ต้องใช้เวลาและกำลังคนในการทดสอบค่อยข้างมาก เพราะว่าในส่วนของการแสดงผลนั้นมันยากมาก ๆ สำหรับการทดสอบ มีหลายสิ่งอย่างให้พิจารณา ยกตัวอย่างเช่น ในแต่ละ element แต่ละส่วนงานทำงานและแสดงผลได้อย่างถูกต้องแต่เมื่อนำมารวมกัน กลับทำงานไม่ถูกต้อง หรือ แสดงผลผิดพลาด การลดเวลาและต้นทุนในการทดสอบลงโดยใช้การทดสอบแบบอัตโนมัติ เป็นสิ่งจำเป็น และต้องมีเครื่องมือช่วย

    ในบทความนี้ขอแนะนำ Appraise ซึ่ง Appraise นั้นได้นำแนวทางของ Specification by Example มาใช้ นั่นหมายความว่า ในแต่ละ test case ต้องมี concrete example ที่ชัดเจน นำ test case เหล่านี้ไปทดสอบแบบอัตโนมัติ ทำการทดสอบด้วย Google Chrome Headless ซึ่งจะทำการ snapshot ส่วนที่ต้องการเป็นรูปภาพและเปรียบเทียบกับผลที่คาดหวังหรือไม่ ถ้าผลออกมาไม่ตรงกับที่คาดหวัง Appraise จะแสดงผลที่แตกต่างออกมาให้เห็น จากนั้นจะให้คนมา approve ว่าผลที่แตกต่างถูกหรือไม่ต่อไป ถ้าทำการยอมรับความแตกต่างก็จะบันทึกผลใหม่ให้ทันที ซึ่งง่ายต่อการดูแลรักษา test case อย่างมาก

    เริ่มต้นการใช้งาน Appraise

    ทำการติดตั้ง Appraise ด้วย NPM

    npm install appraise -g

    จะทำการติดตั้ง Appraise เป็น global command line uitlity อย่างไรก็ตาม สามารถติตั้งเป็นแบบ NPM package dependency โดยใช้ NPM script เรียกใช้ commamd line ก็ได้เช่นกัน

    สร้าง test page

    เริ่มต้นสร้าง folder “examples” เป็นที่เก็บ test page  (default folder สำหรับเก็บ test page ของ Appraise คือ “examples”)  Appraise ใช้  Github-flavoured Markdown syntax  ในการประมวลผล test page

    สร้าง file : hello-world.md ใน folder “examples” และเพิ่มข้อความดังนี้


    This paragraph is just an intro, it will be ignored for testing
    ~~~yaml example="first" fixture="hello.js"
    color: blue
    ~~~

    สร้าง test fixture

    test fixture ใช้สำหรับ Appraise สำหรับประมวลผล example ไปเป็น test result โดยสร้าง file : hello.js ใน folder “examples”และเพิ่ม code ดังนี้

    ทำการ run ทดสอบ

    ทดสอบโดยใช้คำสั่ง

    appraise run

    ซึ่งจะได้ผลดังนี้

    Appraise บันทึกผลการทดสอบไว้ใน folder “results” ซึ่งได้ผลการทดสอบ file : result/hello-world.htm ดังนี้

    จะเห็นได้ว่าผลการทดสอบ failed เนื่องจาก test case นี้ยังไม่ได้กำหนด expected result หรือผลที่คาดหวัง อย่างไรก็ตาม ถ้าพิจารณาผลการทำงานว่าใช้ได้ Appraise มีทางเลือกให้สามารถ capture ผลเพื่อใช้ในการทดสอบครั้งต่อไป โดยทำการ approve ผลการทำงานนี้ก่อนเพื่อทำการยอมรับผลนี้ ด้วยคำสั่ง

    appraise approve --page "hello-world"

    Appraise จะทำการบันทึกผลเป็นรูปภาพลงใน  folder “examples” และแก้ไข hello-world.md กำหนด expected result ดังนี้

    จากนั้นมาเริ่มทดสอบอีกครั้งโดยใช้คำสั่ง

    appraise run

    ซึ่งจะได้ผลดังนี้

    เข้าไปดูผลการทดสอบ file : result/hello-world.htm อีกครั้งดังนี้

    หากทำการแก้ไข test fixture โดยการเพิ่มขนาดตัวอักษร จากนั้นมาเริ่มทดสอบอีกครั้งโดยใช้คำสั่ง

    appraise run

    ก็จะได้ผลดังนี้

    ซึ่งแสดงให้เห็นว่า ผลการทดสอบ failed จากขนาดตัวอักษรที่เปลี่ยนไป ไม่เป็นไปตามที่ต้องการ

     

    อ้างอิง
    https://github.com/AppraiseQA/appraise

    แนะนำ Appraise สำหรับ Visual Testing แบบอัตโนมัติ


  • Stencil : Web component compiler

    Stencil เป็น compiler ที่ทำหน้าที่สร้าง standards-based web component (custom element) ซึ่งรวบรวมแนวคิดที่ดีของ framework ต่างๆที่ได้รับความนิยมสูง มาไว้ใน component โดย build-time tool ที่ใช้งานง่าย เช่น

    • Virtual DOM
    • Async rendering (inspired by React Fiber)
    • Reactive data-binding
    • TypeScript
    • JSX

    web component ที่ได้จาก Stencil เป็น standards-based web component ทำให้สามารถใช้งานร่วมกับ framework ต่างๆที่ได้รับความนิยม และสามารถใช้ได้โดยไม่มี framework ก็ได้ จากเดิมเมื่อพัฒนาด้วย framework หนึ่งแล้ว ไม่สามารถนำไปใช้ร่วมกับ framework อื่นได้

    Stencil มี APIs เช่น Virtual DOM, JSX, และ async rendering  ที่ทำให้สามารถพัฒนา web component ที่ทำงานได้เร็ว มีประสิทธิภาพดีกว่า และสร้างได้ง่ายกว่าเมื่อเปรียบเทียบกับการเขียน custom element โดยตรงโดยไม่ใช้ Stencil  และใน Stencil ยังมี small dev server พร้อมความสามารถ live reload อยู่ด้วย

    เริ่มต้นสร้าง project กับ Stencil

    การสร้าง component ทำได้โดยเริ่มต้นจาก component starter ดังนี้

    git clone https://github.com/ionic-team/stencil-component-starter my-component
    cd my-component
    git remote rm origin
    npm install
    

    จากนั้น ถ้าต้องการ start  live-reload server สำหรับการพัฒนา ให้ใช้คำสั่ง

    npm start

    Updating Stencil
    ถ้าต้องการ update Stencil เป็น version ล่าสุดให้ใช้คำสั่ง

    npm install @stencil/core@latest --save-exact

    Stencil components

    component ถูกสร้างโดยการสร้าง file .tsx ใน “src/components” directory เขียน component ด้วย JSX และ Typescript  ซึ่ง component ที่สร้างมาจาก component starter คือ my-component.tsx

    import { Component, Prop } from '@stencil/core';
    
    @Component({
      tag: 'my-first-component',
      styleUrl: 'my-first-component.scss'
    })
    export class MyComponent {
    
      @Prop() name: string;
    
      render() {
        return (My name is {this.name}); 
      } 
    }
    

    เมื่อ compile แล้วเสร็จ สามารถนำ component ไปใช้ใน HTML page ได้เช่นเดียวกับ tag อื่นๆ Web Components จะต้องมี “-” ภายใน tag (“myFirstComponent” เป็นชื่อที่ไม่สามารถใช้งานได้)

    <my-first-component name="Max"></my-first-component> 
    

    เมื่อเปิดผ่าน browser จะแสดงผล My name is Max

     

    อ้างอิง : https://stenciljs.com

  • Download multiple files as Zip Archive (Compressed) file in ASP.Net MVC

    ในบทความนี้จะขอกล่าวถึงการ  download file ครั้งละหลายๆไฟล์ โดยมีการระบุว่าต้องการ   download file ใดบ้าง โดย  ผู้ใช้สามารถกดเลือกได้ทั้งละหลายๆไฟล์ หรือไฟล์เดียว แล้วแต่ความต้องการของผู้ใช้งาน โดยหลังจากที่มีการกดปุ่มแล้วระบบจะทำการ zip ไฟล์รวมเป็น 1 ไฟล์ โดยในบทความนี้จะขอเสนอวิธีการพัฒนาโดยใช้ ASP.NET  ในรูปแบบ MVC ค่ะ

     

     

    ในส่วนของ java script

    function DownloadFiles() {

    var items = [];

    $(“input:checkbox[name=chkThis]:checked”).each(function () {

    items.push($(this).val());

     

    });

     

    if (items.length <= 0) {

    alert(‘กรุณาเลือกข้อมูลที่ต้องการ Download ด้วยค่ะ/ครับ!!’);

    }

    else {

    $.ajax({

    type: ‘POST’,

    contentType: ‘application/json; charset=utf-8’,

    url: ‘@Url.Action(“DownloadAndZipFile”, “NoteUpload”)’,

    data: JSON.stringify({ fileItemsAll: items }),

    success: function (resultDownload) {

    //window.location = ‘/NoteUpload/Download?fileGuid=’ + resultDownload.FileGuid

    //                   + ‘&filename=’ + resultDownload.FileName;

    window.location = ‘/NoteUpload/Download?fileGuid=’ + resultDownload.FileGuid;

    },

    error: function (data) {

    alert(data);

    }

    });

    }

    //   return items;

    }

    ในส่วนของ controller

    public ActionResult DownloadAndZipFile(IEnumerable<int> fileItemsAll)

    {

    if (!(ViewBag.IsAuthorized = (azResult = azService.Authorize(AccountingOperation.NoteUploadReader, this).Result).IsAuthorize))

    {

    Danger(string.Format(“ไม่มีสิทธิ์ใช้งานในส่วนนี้: {0} ({1})”, azResult.Operation.OP_NAME, Convert.ToString(azResult.OperationEnum)));

    return View();

    }

     

    string handle = Guid.NewGuid().ToString();

    MemoryStream output = new MemoryStream();

     

    var zipMs = new MemoryStream();

    string strZipName = “Accounting” + DateTime.Now.ToString(“yyyyMMdd”);

     

    TempData[handle] = fileItemsAll;

     

    var resultDownload = new { FileGuid = handle, FileName = strZipName + “.zip” };

    return this.Json(resultDownload, JsonRequestBehavior.AllowGet);

    }

     

    public FileResult Download(string fileGuid)

    {

    if (!(ViewBag.IsAuthorized = (azResult = azService.Authorize(AccountingOperation.NoteUploadReader, this).Result).IsAuthorize))

    {

    Danger(string.Format(“ไม่มีสิทธิ์ใช้งานในส่วนนี้: {0} ({1})”, azResult.Operation.OP_NAME, Convert.ToString(azResult.OperationEnum)));

    return null;

    }

     

    var zipMs = new MemoryStream();

    string zipFisYear, zipPeriod, fileC, FileF, FileFinance, fileID, fileNameZip = null;

     

    if (TempData[fileGuid] != null)

    {

    using (ZipFile zip = new ZipFile())

    {

    foreach (var item in TempData[fileGuid] as IEnumerable<int>)

    {

    var dataItemSelect = db.NoteUploadViews.Where(g => g.ID == item).FirstOrDefault();

    var financeID = db.FinanceBudgets.Where(g => g.FINANCE_BUDGET_ID == dataItemSelect.FINANCE_BUDGET_ID).FirstOrDefault();

    zipFisYear = dataItemSelect.FISCAL_YEAR_ID.ToString();

    zipPeriod = dataItemSelect.QUARTER.ToString();

     

    var dataORG = db.Organizations.Where(g => g.ORG_ID == dataItemSelect.ORG_ID && g.FISCAL_YEAR_ID == dataItemSelect.FISCAL_YEAR_ID).FirstOrDefault();

    fileC = Right(“000” + dataORG.ORG_ID, 3);

    FileF = Right(“000” + dataItemSelect.ORG_ID, 3);

    FileFinance = Right(“000” + financeID.FINANCE_ID.ToString(), 3);

    zipFisYear = Right(“0000” + dataItemSelect.FISCAL_YEAR_ID.ToString(), 4);

    zipPeriod = dataItemSelect.QUARTER.ToString();

    fileNameZip = zipFisYear + zipPeriod + “.zip”; //JAR+ Format for Zip name = FiscalYear|PeriodID >> 25601

     

    var fileType = dataItemSelect.FILE_TYPE;

    var fileNameInzip = fileC + FileF + FileFinance + dataItemSelect.ID.ToString() + “.” + fileType;  //JAR+  Format for  File Name = Campus|Fac|Finance|ID >> C01F010031

     

    byte[] fileDatas = (byte[])dataItemSelect.FILE_DATA;

     

    zip.AddEntry(fileNameInzip, fileDatas);

    }

     

    zip.Save(zipMs);

     

    byte[] fileData = zipMs.GetBuffer();

    zipMs.Seek(0, SeekOrigin.Begin);

     

    zipMs.Flush();

    Response.Clear();

    Response.AddHeader(“content-disposition”, “attachment;filename=” + fileNameZip);

    Response.BinaryWrite(fileData);

    Response.End();

    }

    }

     

    return File(zipMs, “application/zip”);

    }

    จากตัวอย่างข้างต้นจะเห็นว่าในปัจจุบัน มีความต้องการไม่น้อยที่จะ download ไฟล์หลายๆไฟล์ แล้วรวมเป็นไฟล์เดียว เพื่อความสะดวกในการจัดเก็บข้อมูล และวิธีการพัฒนาก็ไม่ได้ยุ่งยาก ผู้เขียนจึงหวังเป็นอย่างยิ่งว่าจะเป็นประโยชน์ไม่มากกว่าน้อยสำหรับผู้พัฒนาในรูปแบบ MVC นะคะ

     

     

     แหล่งอ้างอิง

    https://www.aspsnippets.com/Articles/Download-multiple-files-as-Zip-Archive-Compressed-file-in-ASPNet-MVC.aspx

    https://www.carlrippon.com/zipping-up-files-from-a-memorystream/