Tag: pyramid

Hello Pyramid [Part 2] – Test First

Posted by – January 17, 2013

Post ก่อนเรามี test แล้ว ตอนนี้เราต้อง implement code เราให้เข้ากับ test นั่น

Model

พระเอกของเราจริงๆ ที่น่าจะพูดถึงก่อนน่าจะเป็น SQLAlchemy นอกจากการบันทึกที่มีรายละเอียดนิดหน่อยแล้ว ยังมีเรือ่ง transaction เข้ามาเกี่ยวด้วย คือมันเหมือนจะไม่มีอะไร แต่ถ้าเป็น code template ที่ generate มาจาก tool ของ Pyramid มันจะมี transaction ที่แปลกออกไป

เริ่มที่การประกาศ fields ก่อน

Base = declarative_base()
class Todo(Base):
    __tablename__ = 'todos'
    id = Column(Integer, primary_key=True)
    task = Column(String(512), nullable=False)
    created_at = Column(DateTime, server_default=text('NOW()'), nullable=False)
    done_at = Column(DateTime)
    priority = Column(Integer, default=5) # 1 => the most priority, 10 => not important now 
 
    def __init__(self, task, done_at=None, priority=1):
        super(Todo, self).__init__()
        self.task = task
        self.done_at = done_at
        self.priority = priority

ก็ง่ายๆ มี fields บอกรายละเอียด บันทึกเวลาตอนสร้าง เสร็จตอนไหน แล้วก็ระดับความสำคัญของ todo

อีกหนึ่งความมึนคือ transaction

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))

code บรรทัดบน จะทำการสร้างการเชื่อมต่อสำหรับฐานข้อมูลซึ่งส่ง ZopeTransactionExtension ทำหน้าที่ commit transaction หลังจากที่ process งานตาม request ที่มีมา ถ้ามีการ raise เกิดขึ้น จะมีการ rollback สิ่งที่เราสั่งไป

ปัญหาคือ ถ้าเราทำตามตัวอย่างในเว็บมันเนี่ย มันจะให้เราสร้าง transaction เองมาใหม่ด้วย transaction.manager พอจบ scope ของ transaction.manager มันจะ commit ทีนี้ ตอนเรา tear down เราจะไม่มีอะไรให้ rollback ที่ผมต้องเพิ่ม DBSession.remove() มาก็เพื่อจะสั่งลบข้อมูลหลังจาก test เสร็จ ซึ่งตัวอย่างใน doc มันไม่ต้องเพราะมัน test ด้วย sqlite ที่เป็นแค่ฐานข้อมูลในหน่วยความจำชั่วคราว จบการ test เสร็จมันก็เคลียร์ไปเลย

ทีนี้ มันมีกระบวณท่าเพิ่มเข้ามาอีกเพื่อทำให้การ test ราบรื่น ต้องมาเริ่มที่ ORM method ของ SQLAlchemy ไม่ว่าจะสั่งให้เพิ่มหรือลบอะไรผ่าน session ของ SQLAlchemy มันจะเหมือนเราสั่งเก็บไว้ใน stack ก่อน แล้วค่อย execute ทีเดียว แต่สิ่งที่ผมคาดหวังไว้ คือ หลังจาก save แล้ว ผมควรจะได้ผลการ save ณ ตอนนั้นเลย วิธีการง่ายๆ คือใช้คำสั่ง

DBSession.flush()

พอ flush แล้ว todo instance ก็จะมีการเซ็ตค่าหลังจากมีการบันทึก เช่น ถ้าหาก new todo ยังไม่มี id จะถือว่าเป็น create พอ flush แล้ว instance นี้จะมี id กำกับตาม id ในฐานข้อมูล หากไม่ flush ก็ไม่มี id ก็จะไม่รู้เลยว่าบันทึกผ่านหรือเปล่า ต้องมาคอยจับ exception เอา ซึ่งตอนนี้ยังไม่รู้เลยว่าจะจับยังไง เพราะมันจะ execute ตามที่อธิบายไปก่อนหน้านี้และอยู่นอก scope ของ code ด้วย

กลับมาเรื่อง code ต่อ มันคงไม่สนุก ถ้าหากจะต้องมา flush ทุกครั้งหลังจากการบันทีกหรือลบ ผมเลยห่อมันไว้เป็น class แม่ซะ พร้อมทั้งเพิ่มส่วนของ validation helper ไปด้วย ที่ SQLAlchemy ไม่มี

class AppBase(object):
    _errors = None
 
    def save(self):
        if self.is_valid:
            try:
                DBSession.add(self)
                DBSession.flush()
                return True
            except IntegrityError:
                return False
        return False
 
    def update(self):
        if self.is_valid:
            try:
                DBSession.merge(self)
                DBSession.flush()
                return True
            except IntegrityError:
                return False
        return False
 
    def delete(self):
        try:
            DBSession.delete(self)
            DBSession.flush()
        except IntegrityError: # still dont have an idea to test this failure
            return False
        return True

และส่วนของ validation ที่เพิ่มเข้ามาเอง

    @property
    def errors(self):
        return self._errors
 
    @property
    def is_valid(self):
        return not bool(self._errors)
 
    def validate(self, validator, key, value):
        if not self._errors:
            self._errors = {}
        try:
            validator.to_python(value)
        except Invalid as e:
            if not self._errors.get(key):
                self._errors[key] = []
            self._errors[key].append(str(e))

SQLAlchemy มี hook ไว้ที่นึงชื่อ validates เป้น decorator จะถูกเรียกตอนที่ assign ค่า

    @validates('task')
    def validate_task(self, key, value):
        # validate
        return value

โดย task ก็คือ field ที่เราระบุไว้ใน Todo class โดย method นี้จะอยู่ใน Todo class เช่นกัน โดยเชื่อมระหว่าง class แม่นี้ กับ Todo ด้วย

Base = declarative_base(cls=AppBase)

และ code ใน models.py ก็จะมีหน้าตาเป็นแบบนี้

from sqlalchemy import (
    Column,
    Integer,
    Text,
    String,
    DateTime,
    Integer
    )
 
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.expression import text
from sqlalchemy.exc import IntegrityError
 
from sqlalchemy.orm import (
    scoped_session,
    sessionmaker,
    validates,
    )
 
from zope.sqlalchemy import ZopeTransactionExtension
 
from formencode import validators, Invalid
 
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
#DBSession = scoped_session(sessionmaker())
 
class AppBase(object):
    _errors = None
 
    def save(self):
        if self.is_valid:
            try:
                DBSession.add(self)
                DBSession.flush()
                return True
            except IntegrityError:
                return False
        return False
 
    def update(self):
        if self.is_valid:
            try:
                DBSession.merge(self)
                DBSession.flush()
                return True
            except IntegrityError:
                return False
        return False
 
    def delete(self):
        try:
            DBSession.delete(self)
            DBSession.flush()
        except IntegrityError: # still dont have an idea to test this failure
            return False
        return True
 
    @property
    def errors(self):
        return self._errors
 
    @property
    def is_valid(self):
        return not bool(self._errors)
 
    def validate(self, validator, key, value):
        if not self._errors:
            self._errors = {}
        try:
            validator.to_python(value)
        except Invalid as e:
            if not self._errors.get(key):
                self._errors[key] = []
            self._errors[key].append(str(e))
 
Base = declarative_base(cls=AppBase)
 
class Todo(Base):
    __tablename__ = 'todos'
    id = Column(Integer, primary_key=True)
    task = Column(String(512), nullable=False)
    created_at = Column(DateTime, server_default=text('NOW()'), nullable=False)
    done_at = Column(DateTime)
    priority = Column(Integer, default=5) # 1 => the most priority, 10 => not important now 
 
    def __init__(self, task, done_at=None, priority=1):
        super(Todo, self).__init__()
        self.task = task
        self.done_at = done_at
        self.priority = priority
 
    @validates('task')
    def validate_task(self, key, value):
        self.validate(validators.String(not_empty=True), key, value)
        return value
 
    @validates('priority')
    def validate_priority(self, key, value):
        self.validate(validators.Int(), key, value)
        return value

โดยตัว validation ที่ผมเลือกเข้ามาช่วยคือ formencode ที่ validate ได้ค่อนข้างครอบคลุม อย่างในตัวอย่าง ผมต้องการแค่ validators.String(not_empty=True) คือ ห้ามเป็นค่าว่าง กับ validators.Int() คือต้องเป็น Int เท่านั้น

Controller

Framework ฝั่ง Python เนี่ย มักจะใช้คำว่า views แทน controllers ซึ่งผมไม่ค่อยชอบเท่าไหร่ มันแปลกๆ กับ MVC เลยเปลีย่นเป็น controller(s) ซะเลย โดยสร้าง package เป็น controllers แล้วแยกเป็น file ไป โดยผมตั้งชื่อให้เป็น todos.py ตามใน test ที่มี

from todolist.controllers.todos import create

ใส่ไว้

มีท่ายากมานิดหน่อยสำหรับ template ที่จะใช้ เนื่องจาก template engine ตัว builtin มันเลยคือ ZPT(Chameleon) แต่มันไปติด bug นี้ ปัญหาคือ ใช้ macro แล้วใส่ doctype ไม่ได้ ผมเลยเปลี่ยนมาใช้ mako แทน

Read

ในส่วนนี้จะเป็นหน้า list ของ todo

def get_todo_set():
    todos = DBSession.query(Todo).filter(Todo.done_at==None).order_by(Todo.priority.desc()).all()
    done_todos = DBSession.query(Todo).filter(Todo.done_at!=None).order_by(Todo.priority.desc()).all()
    return todos, done_todos
 
@view_config(route_name='todo_index', renderer='todos/index.mako')
def index(request):
    todos, done_todos = get_todo_set()
    return dict(todos=todos, done_todos=done_todos)

ผมห่อ get_todo_set แยกไว้ เพราะจะมีการเรียกใช้ที่อื่นด้วย ถ้าเปิดตัว test เทียบดูก็จะเห็น

        self.assertEqual(len(response['todos']), 3)
        self.assertEqual(len(response['done_todos']), 1)
        # test ordering
        self.assertEqual(response['todos'][0].task, "First task")
        self.assertEqual(response['todos'][2].task, "Second task")

จะเห็นว่ามีการเช็คด่ากับ response ซึ่งเป็นสิ่งที่ method ส่งกลับไป
ใน method ที่เหลือ ก็ลองเปิด test เทียบเอานะครับ

code นี้จะมี @view_config เป็น decorator ทำหน้าที่ config หน้าที่หลายๆ อย่างสำหรับ controller(หรือ view) เบื้องต้นทีใช้ในตัวอย่างก็มี

  • route_name ไว้เชื่อมกับการกำหนด path ของ uri ซึ่งต้องกำหนดก่อน
  • renderer ไว้กำหนดไฟล์ที่จะ render หรือถ้ากำหนดเป็น xml หรือ json มันจะแปลงข้อมูลจะส่งไปเป็น xml หรือ json ให้เลย
  • request_method ไว้กำหนด request method
  • xhr ไว้กำหนดให้รับ request ที่เป็น ajax

หรือดู list ทั้งหมดได้ที่ doc

สำหรับการ test ในสวนของ @view_config มีความจำเป็นต้องไป test ด้วย functional test ครับ เช่น test ว่า status เป็น 200 หรือเปล่า หรือคืน content-type เป็น json หรือเปล่า จำพวกนี้ครับ

Create
@view_config(route_name='todo_create', renderer='json', request_method='POST', xhr=True)
def create(request):
    todo = Todo(
        task=request.POST.get('task', None),
        priority=request.POST.get('priority', None)
    )
    if not todo.save():
        return {'errors': todo.errors}
    return {'id': todo.id, 'task': todo.task, 'priority':todo.priority, 'messages': '%s has been created' % todo.task }

จะเห็นว่าต้องการรับ request มาเป้น ajax เท่านั้น
และการยืนยันว่า todo ที่สร้าง ถูกสร้างอย่างแน่นอนคือถ้าสร้างเสร็จแล้วจะต้องมีการเซ็ทค่า id ให้กับ todo ตามที่ผมอธิบายไปข้างบน คือ ถ้าไม่ flush() ก็จะไม่รู้เลยว่ามันบันทึกจริงๆ หรือเปล่า จะจับ exception ก็ยังคิดท่าไม่ออก แต่ข้อดีของการเช็คแบบนี้คือ มันเช็คได้ละเอียดแยกเป็นขั้นตอนได้ดีกว่า

Update
@view_config(route_name='todo_update', renderer='json', request_method='POST', xhr=True)
def update(request):
    todo_id = request.matchdict['id']
    try:
        todo = DBSession.query(Todo).filter_by(id=todo_id).one()
    except NoResultFound:
        return {'errors': "No todo id: %s" % todo_id}
 
    todo.task=request.POST.get('task', None)
    todo.priority=request.POST.get('priority', None)
 
    if not todo.update():
        return {'errors': todo.errors}
 
    return {'task': todo.task, 'priority':todo.priority, 'messages': '%s has been updated' % todo.task}

เบี้องหลังของการ update จริงๆ คือ merge() โดยการใส่ instance ลงไปใน DBSession ถ้ามี id อยู่ จะทำการเปลี่ยนค่า แต่ถ้าไม่มี id มันจะสร้างใหม่ให้เลย

Delete
@view_config(route_name='todo_delete', renderer='json', request_method='POST', xhr=True)
def delete(request):
    todo_id = request.matchdict['id']
    try:
        todo = DBSession.query(Todo).filter_by(id=todo_id).one()
    except NoResultFound:
        return {'errors': "No todo id: %s" % todo_id}
    if not todo.delete():
        return {'errors': "%s can't be deleted" % todo.task}
 
    return {'id': todo.id, 'messages': '%s has been deleted' % todo.task}

และอันสุดท้ายก็เป็นการลบ พร้อมกับข้อควมต่างๆ กรณีที่มีปัญหา

จริงๆ ผมตั้งใจจะทำฝั่ง UI ให้ดูด้วย เป็น ajax แต่พอทำไปสักพักแล้วรู้สึกนานไปละ และผมอยากให้ดูแค่เรื่องของการ test ก็เลยพักไว้แค่นี้

สรุป

การ test เบี้องต้นผมพอใจนะ สำหรับการ test แค่ integration test เครืองมืออย่าง nosetest ไว้ทำ code coverage เอามารวมปับ unittest ถือว่าสะดวกสบายพอสมควร ตัว unitest เองก็ test ได้ครอบคลุมดี อาจจะเพราะผมเองมือใหม่ บางท่าอาจจะยังไม่ถูกต้องนัก โดยเฉพาะกับ SQLAlchemy ถ้ามีโอกาสอยู่กับมันนานๆ คงจะมีท่าดีๆ กว่านี้

สามารถ Download source code มาดูทั้งหมดได้ครับ

hg clone https://bitbucket.org/punneng/pyramid-testfirst

หรือจะ folk กันก็เอาตามสะดวก

ไว้มีเวลาจะมาลอง functional test ให้ดูครับ ตอนนี้ขอสลับโหมดไป Rails ก่อน :) เลี้ยงปากเลี้ยงท้องหน่อยครับ

Hello Pyramid [Part 1] – Test First

Posted by – December 19, 2012

เคยมองหา framework ทางเลือกที่คล้ายๆ Ruby on Rails บน Python ขอแค่มี MVC  มี config น้อยๆ แต่ถ้าจะปรับ ต้องทำได้ มีการจัดการ Routing และเครืองมือสำหรับเขียน Test ที่สนใจก็มี Django, web2py และ Pyramid ซึ่ง Django ก็ใช้ทำงานอยู่ และ Pyramid ก็ดูเบากว่า web2py เลยเลือกตัวนี้

หลังจากอยู่กับมันได้ 2-3 อาทิตย์ พบว่าเป็น framework ที่ไม่ซับซ้อนอะไรมาก เอกสารค่อนข้างชัดเจน แต่ความยากสำหรับคนฝั่ง Rails อย่างผม คือ ผมขี้เกียจหา ORM ดูอยู่นาน ซึ่งผมเลือก SQLalchemy วัดดวงกับมัน ลองดูจริงๆ ก็ไม่ได้เลวร้ายอะไรมาก แค่ต้องทำความคุ้นเคยกับการอ่านเอกสารนิดหน่อย ตั้งแต่เริ่มเขียนเว็บ อ่านเอกสารมาก็เยอะ เพิ่งเจอว่าเอกสารของ Pyramid มีการเขียน test ในหน้าแรกๆ ของเอกสารเลย ยิ่งประทับใจเข้าไปใหญ่ แค่แอบงงกับการจัดหมวดหมู่ของ Test ของมันนิดหน่อย จริงๆ อยากได้ BDD แบบ RSpec เลย แต่ยังหาไม่ได้

ผมจะเริ่มต้นด้วยการจินตนาการก่อน ว่าผมจะเขียน Test ไปให้ครอบคลุมกับ CRUD ของ Todolist ขำๆ ตัว test ของผมจะต้อง fail แล้วผมจะค่อยๆ implement ไป ทำให้มันผ่าน ซึ่งการเริ่มต้นของ project ขอให้ setup ตาม doc นี้ก่อน พอติดตั้งแล้วจะเจอเครืองมือสำหรับ Test 2 ตัวคือ Unittest กับ Test Coverage โดยตัว Unittest ก็เป็น Unittest ของ Python เอง(หรือ PyUnit) ส่วนตัว Test Coverage เป็นการรวมตัวของ Coverage ที่เอาไว้วัดว่า code ถูกเรียกใช้ขนาดไหนเมื่อเทียบกับ code ทั้งหมด กับ nose ที่จะทำให้เรากำหนดว่าเราจะเรียก test file ไหนบ้าง ก่อนที่จะไปดูตัวอย่าง ต้องมาพููดถึงระดับหรือมุมมองการ test ก่อน ใน Pyramid มันแบ่งเป็น 3 แบบคือ

  • Unit test คือการ test ในส่วนย่อยสุด(unit) เช่น function เฉพาะงานหรือตัวแปร
  • Integration test คือการ test ที่รวมสิ่งทีเ่ป็น unit ข้างบน มา test รวมกัน ซื่งในตัวอย่างจะเป็น Integration test ทั้งหมด
  • Functional test คือการ test เหมือน integration test แต่ run ด้วย code จริง มี stack กี่ชั้นก็ทำงานหมด ไม่มีการ mock/stub ไว้ก่อน

My test cases

จากนั้น มานึกดูว่า test เราควรจะมีส่วนอะไรบ้าง..

  • C – Create ควรจะส่ง request ไปที่ function แล้วคืนค่ามาเป็น dict ที่บรรจุข้อมูลของ todo ที่สร้าง
  • R – Read(also Edit and list) ควรจะ get ไป server แล้วคืนค่ามาเป็น dict ของ id หรือ list ของ dict ของ todo
  • U – Update ใจจริงอยากให้เป็น restful ด้วยการทำให้เป็น put แตผมเพิ่งเริ่ม ท่าพลิกแพลงไว้คราวหลัง ตอนนี้ขอเป็น post ชุดของข้อมูลทีจะ update แล้วคืนค่ามาเป็น dict ของ todo ที่ update แล้ว
  • D – Delete ให้เป็น post ไป server แล้วคืนค่ามาเป็น id ของัตวที่ถูกลบ

สังเกตดู สิ่งที่ผมพิจารณาคือ มันคืนค่ามาเป็นอะไรใน function ที่เตรียมไว้ ซึ่งจริงๆ มันควรจะมี http status เป็น 200 หรือ ข้อมูลที่คืนมาควจะมี content-type เป็น json ซึ่งจริงๆ ตรงส่วนนี้จะมีเครืองมีที่จำลอง http request จริงๆ ส่งไป test ใน functional test อีกที คืิดว่าคงเห็นภาพที่มันพยายามแบ่ง Integration/Functional test ไว้แล้ว

Set up and Tear down

อีกสักนิด จะขาดไม่ได้ ต้องแนะนำสิ่งทีจำเป็นใน Unittest ก่อน… set up/tear down เป็นพื้นที่สำหรับใส่ code ทีจะให้ถูกเรียกใช้ตอนที่เริ่ม test กับหลัง test ซึ่งโดยทั่วไป มักจะเป็นการเริ่มคำสั่งเตรียม stack ของการ test หรือใส่ชุดข้อมูลสำหรับ test ลงใน database ใน set up พอ test เสร็จแล้ว ก็ต้องเคลียร์ออกที่ใส่ไป ใน tear down หน้าตาประมาณนี้

class TestIntegrationTodolist(unittest.TestCase):
    def setUp(self):
        self.config = testing.setUp()
        from sqlalchemy import create_engine
        #engine = create_engine('sqlite:///todolist.sqlite')
        engine = create_engine('postgresql://dev:dev@localhost:5432/todolist_test')
 
        DBSession.configure(bind=engine)
        Base.metadata.create_all(engine)
        # the transaction commited after added so rolling back doesnt work on Postgres
        #with transaction.manager:
        instances = (
            Todo(task='Second task', priority=0),
            Todo(task='Thrid task', priority=5),
            Todo(task='First task', priority=10),
            Todo(task="Done task", priority=5, done_at=datetime.now())
        )
        DBSession.add_all(instances)
 
    def tearDown(self):
        DBSession.remove()
        testing.tearDown()

จากตัวอย่างข้างบน จะเริ่มต้นจากการต่อเข้า PosgreSQL จากนั้นก็เชื่อมต่อระหว่าง Pyramid กับ SQLAlchemy แล้วจึงใส่ code ในส่วนของการเตรียมขอ้มูลสำหรับทดสอบลงไป พอเทสจบ ก็เคลียร์ค่าเก่าไป

Read

การ read ใน todolist ของผม จะมีแค่ 1 งานเท่านั้น คือการแสดงรายการใน todolist ทั้งหมด สิ่งที่ผมสนใจคือ พอ request แล้ว มันควรจะคืนค่ามาเป็นชุดของ dict ทีบรรจุข้อมูลของ todo แต่ละอัน

    def test_index_pass(self):
        """ test_index_pass """
        from todolist.controllers.todos import index
        request = testing.DummyRequest()
        response = index(request)
        self.assertEqual(len(response['todos']), 3)
        self.assertEqual(len(response['done_todos']), 1)
        # test ordering
        self.assertEqual(response['todos'][0].task, "First task")
        self.assertEqual(response['todos'][2].task, "Second task")

ก็ตรงไปตรงมา สิ่งที่ดูไม่เหมือนคนอื่นเลยคือจะมี done_todos ซึ่งจริงๆ แล้วผมตั้งใจว่าจะแยกตัวที่ทำไปแล้ว ไปไว้อีกกลุ่มนึง เลยแยกไว้ และทำการใส่ค่าเริ่มต้นจากตอน setup ด้วยการใส่ done_at เป็นการบอกว่าตัวนี้ทำแล้ว

ความสะดวกอย่างนึงของ Pyramid คือ มีการจำลอง request ซึ่งง่ายมากด้วย testing.DummyRequest() ซึ่งถ้าเป็นการ post จะมีในตัวอย่างถัดไป

Create
    def test_create_pass(self):
        """ test_create_pass """
        from todolist.controllers.todos import create
        params = {'task':'New task', 'priority':1}
        request = testing.DummyRequest(params=params, post=params)
        response = create(request)
        self.assertTrue(response['id'])
        self.assertEqual(response['task'], params['task'])
        self.assertEqual(response['priority'], params['priority'])
 
    def test_create_fail(self):
        """ test_create_fail """
        from todolist.controllers.todos import create
        params = {'task':"", 'priority': "low"}
        request = testing.DummyRequest(params=params, post=params)
        response = create(request)
        self.assertTrue(response['errors'])
        self.assertEqual(len(response['errors']['priority']), 1)
        self.assertEqual(len(response['errors']['task']), 1)
        self.assertEqual(response['errors']['task'], ['Please enter a value'])

ส่วนของ create เนี่ย ผมเตรียม test ไว้ 2 กรณี คือ ผ่าน กับ ไม่ผ่าน โดยถ้าไม่ผ่านจะมีข้อความเตือนหน่อยนึง อย่างกรณีนี้ ผมใส่เป็นข้อความเปล่าๆ ไป มันควรจะบอกผมว่า ผมควรใส่ค่าอะไรไปหน่อยนะ

สว่น priority ไว้บอกความเร่งด่วน ค่อนข้างตรงไปตรงมา

ตัวอย่างการ post ด้วย DummyRequest ก็แค่ใส่ค่า post ใน parameter ตอนสร้าง DummyRequest เท่านั้นเอง ส่วน parameter จะมีอะไรบ้าง ก็ลองเปิดอ่าน doc ดูครับ จะเห็นตัวอย่างการจำลอง session ด้วย

Update
    def test_update_pass(self):
        """ test_update_pass """
        from todolist.controllers.todos import update
        params = {'task':'Updated task', 'priority':1}
        # provide the todo with id
        todo = DBSession.query(Todo).first()
        request = testing.DummyRequest(params=params, matchdict={'id':todo.id}, post=params)
        response = update(request)
        updated_todo = DBSession.query(Todo).filter_by(id=todo.id).one()
        self.assertEqual(response['task'], params['task'])
        self.assertEqual(updated_todo.task, params['task'])
 
    def test_update_fail(self):
        """ test_update_fail """
        from todolist.controllers.todos import update
        # test query not found
        request = testing.DummyRequest(params={}, matchdict={'id':1}, post={})
        response = update(request)
        self.assertEqual(response['errors'], "No todo id: 1")
 
        # test validation
        params = {'task':"", 'priority': "low"}
        todo = DBSession.query(Todo).first()
        request = testing.DummyRequest(params=params, matchdict={'id':todo.id}, post=params)
        response = update(request)
        self.assertTrue(response['errors'])
        self.assertEqual(len(response['errors']['priority']), 1)
        self.assertEqual(response['errors']['task'], ['Please enter a value'])
        self.assertEqual(response['errors']['priority'], ['Please enter an integer value'])

เหมือนของ create เลย แตต่างกันตรงที่ตอน test การ validate  ข้อมูล ผมคิดว่าจะให้ priority เป็นตัวเลข แต่ใน test นี้ผมดันใส่เป็นข้อความไป มันควรจะบอกสิว่าให้ใส่ตัวเลขนะ

Delete
    def test_delete_pass(self):
        """ test_delete_pass """
        from todolist.controllers.todos import delete
        todo = DBSession.query(Todo).first()
        request = testing.DummyRequest(params={},matchdict={'id':todo.id}, post={})
        response = delete(request)
        todo_count = DBSession.query(Todo).count()
        self.assertIsNot(todo, DBSession.query(Todo).first())
        self.assertEqual(response['messages'], '%s has been deleted' % todo.task)
        self.assertEqual(todo_count, 3)
 
    def test_delete_fail(self):
        """ test_delete_fail """
        from todolist.controllers.todos import delete
        # test query not found
        request = testing.DummyRequest(params={}, matchdict={'id':1}, post={})
        response = delete(request)
        self.assertEqual(response['errors'], "No todo id: 1")

สำหรับ 2 กรณีอีกเช่นกัน แต่การ test กรณีที่ไม่ผ่าน ก็จะมีข้อความอะไรเล็กหน้อยมาบอกว่าถ้าหากถูกลบไปแล้ว ถ้ากรณีผ่าน จำนวนทั้งหมดควรจะถูกลดไปด้วย

จาก test ทั้งหมดข้างบนเนี่ย พอรันด้วย nosetest หน้าตาจะเป็นแบบนี้

Failed nosetest

Failed nosetest

จะเห็นถึง error นิดหน่อยคือ พยายามจะ import มา แตเรายังไมได้ implement มันก็เลยพัง
อีกส่วนคือหน้าตาของ code coverage

Post ต่อไปจะ implement แล้วครับ