Contract test ควรใช้คู่กับ Unit test ของแต่ละ party นะครับ จะได้ผลลัพธ์ที่ดีที่สุด และแน่นอนว่า E2E test ก็ยังคงมีความจำเป็นอยู่นะครับ แต่บางครั้งเราสามารถใช้ Contract test เข้ามาช่วยเพื่อลดงานลงไม่ต้องถึงขั้นทำ E2E ทุกครั้งไปได้ครับ
ครั้งต่อไปจะลองทำ contract test ง่ายๆ แต่ให้เห็นภาพให้อ่านกันครับ
อยากจะเล่าประสบการ์ณส่วนตัวที่พอเห็นมา เรื่องตำแหน่งของ SA ในช่วงที่เป็นยุคทองของ Dev ยุคนี้
เริ่มจาก SA ชื่อเต็มๆ คือ System Analyst ตำแหน่งนี้ทำอะไรเล่าแบบที่ไม่ยาวจนเกินไปก็...หลายคนจะบอกว่าเป็นนักวิเคราะห์ระบบ(ก็ตามชื่อมัน) แต่จริงๆ แล้วค่อนข้างทำหลายอย่างทีเดียวตั้งแต่
Gathering requirements
System design
Database design
Software document
Skill หลักๆ ที่กล่าวมาจะไม่ได้เน้น coding สักเท่าไหร แต่ๆๆๆ ควรจะผ่านงาน coding มาก่อนนะ ซึ่งถ้าใครเคย Search google เรื่อง SA แล้วมีคนถามเกี่ยวกับ SA ต้อง coding ได้ไม ต้องเป็น Dev มาก่อนไม โดนส่วนด้วยผู้เขียนก็ยังคิดว่าควรจะเป็น Dev มาก่อน แล้วก็ควรจะยังพอ Dev ได้ใน Tech Stack ที่ตัวเองดูแลนะ \
Activities อื่นๆ ที่ SA ต้องเข้าไป Involve ด้วยอีกก็ เช่น meeting, investigate, impact analysis, ...... อาจจะมีถึง project tracking เลยทีเดียว
แบบนี้คิดว่าไม่จำเป็นที่จะต้องมี SA ก็ได้ คงต้องดูบริบทของงานแต่ละคนขององค์กรนั้นๆ อีกที
สมาชิกเป็น Senior Dev และ Junior Dev
แบบนี้แนะนำให้ควรมี SA อยู่ด้วย เนื่องจาก Senior ที่ต้องมาดูแลน้องในทีมด้วยถือเป็นหน้าที่หนักพอด้วย หากต้องดูในเรื่องอื่นๆ เพิ่มด้วยจะส่งผลกับการออก Feature ได้
Water fall(Non Agile)
แบบนี้คิดว่ายังไงก็ยังควรต้องมีอยู่ เพราะกระบวนการและ process ยังเยอะอยู่ มีช่วงเวลาในการเก็บ requirement ค่อนข้างยาวไม่ควรต้องให้ทีม Dev เข้ามา join ตรงนี้เพราะจะ manday ไป free
ตัวอย่าง Scrum ที่มี SA ร่วมทีมจากประสบการ์ณผมเจอมา 2 แบบซึ่งคิดว่าก็ลงตัวในระดับนึง
แบบแรก SA คือ leader ของทีม
ผู้ร่วมทีมจะมี SA, Dev, QA โดยแต่ละทีมจะรับผิดชอบ Feature นั้นๆ ทั้งหมด Cover ทั้ง Frontend และ Backend มี SA ทำหน้าที่เป็น Leader ทำงานใกล้ชิดกับ PO เพื่อสรุป Story เข้า Sprint เมื่อได้รายละเอียดเพียงพอที่จะเอามาทำแล้วก็จะให้ทีม vote point ตอน planing อีกทีว่ารับงานได้แค่ไหน ในช่วงเวลาที่ start sprint SA ก็จะมีหน้าที่ประสานงานและเตรียมในสิ่งที่ต้องใช้ในงาน sprint ร่วมถึงวางแผ่น design งานใน backlog ที่จะเอาเข้ามาทำในรอบต่อไป หน้าที่อีกอย่างคือสรุปงานและเตรียมของรวมถึงแผน rollback สำหรับ deploy งานขึ้น production
SA ในทีมรูปแบบนี้จำเป็นที่จะต้องผ่านการ coding มาก่อนและควรมีความเป็น Senior เนื่องจากทีหน้าที่เป็นผู้นำของทีมและมีความรับผิดชอบทั้งในส่วน design และประสานงาน
แบบสอง SA ทำหน้าที่ Platform/Feature integration
ผู้ร่วมทีมจะมี SA, Dev Lead, Dev, QA โดยที่แต่ละทีมจะทำ Product เดียวกันแต่แบ่งทีมเป็นทีม Frontend และ Backend ทีมที่รับผิดชอบ Frontend จะดูแลในส่วน Feature ส่วนทีมที่รับผิดชอบ Backend จะดูแลในส่วน Platform SA ทำงานใกล้ชิดกับ PO และ Dev Lead เพื่อสรุป Story เข้า Sprint เมื่อได้รายละเอียดเพียงพอถึงเอางานเข้า planing
Frontend ในช่วง start sprint จะมีหน้าที่ค่อย support team ในเรื่องของ flow และ data ที่จะต้อง integrate กับทีมต่างๆ และจะต้องสรุป flow ของ feature ต่อไปที่อยู่ใน backlog เพื่อจะได้เอาเข้ามาทำใน next sprint ได้
Backend ในช่วง start sprint จะมีหน้าที่ค่อย support team ในเรื่อง spec ของ API ที่จะต้องทำและจะต้อง design API ต่อไปที่อยู่ใน backlog ที่ PO ต้องการให้ทำเพิ่มเพื่อ support frontend หรือ partner ที่จะมาเรียกใช้งาน
จะเห็นว่า SA ในรูปแบบการทำงานทีม Frontend ไม่จำเป็นต้องมีทักษะ Coding ก็ได้เนื่องจากจะดูในส่วนของ Flow และมี Dev lead ค่อยให้คำปรึกษา แต่ในส่วนของ Backend ควรจะมีทักษะ Coding อยู่บางเนื่องจากต้อง design API ขึ้นมาถึงแม้จะมี Dev lead ให้ปรึกษาแต่ถ้าไม่มีทักษะ Coding จะทำให้เวลาพูดคุยกันเข้าใจไม่ตรงกันได้
SA ยังจำเป็นต้องมีในทีมหรือไมนั้นขึ้นอยู่กับรูปแบบการทำงานของทีมขององค์กรนั้นแต่ SA จำเป็นที่จะต้องมี Skill coding ติดตัวในยุคนี้ครับ ถือว่าเป็น Skill จำเป็นที่ต้องมีเลยดีกว่า เพราะจะทำให้ SA ได้ทำหน้าที่ของ SA จริงๆ และช่วยทำให้ทีมเป็นสุขครับ
request กับ request-promise จากเว็บต้นทางแจ้งว่า "This package has been deprecated" แล้วนะครับ ดังนั้นถ้านำไปใช้ใน production น่าจะต้องหา package ที่เทียบเท่ามาใช้งานแทบครับ
function getDefineName(valueRefStr) { var ref = valueRefStr; var tmp = ref.split('/'); var defineName = tmp[tmp.length - 1]; return defineName; }
4.6. Function สำหรับ Set ค่า Request สำหรับนำไปสร้าง Excel
function pushDataToModelRequest(jsonPopDefineList,paramName,paramType,dataType,paramLv,paramDesc,paramRequired,paramSimpleValue,paramPosible) { var requestModel = new modelService.modelRequest(); requestModel.name = paramName; requestModel.paramType = paramType; requestModel.type = dataType; requestModel.lv = paramLv; if (paramDesc) { requestModel.description = paramDesc; } else { requestModel.description = ''; } if (paramRequired) { requestModel.required = paramRequired; } else { requestModel.required = checkDefinitionRequestIsRequired(jsonPopDefineList,paramName); } requestModel.simpleValue = paramSimpleValue; var posibleStr = ''; if (paramPosible) { for (var eName in paramPosible) { posibleStr += paramPosible[eName] + ','; } if (posibleStr != '') { posibleStr = posibleStr.substr(0,posibleStr.length -1); } } requestModel.posibleValue = posibleStr; listDefineRequest.push(requestModel); }
4.7. Function สำหรับ Set ค่า Response สำหรับนำไปสร้าง Excel
function pushDataToModelResponse(paramName,paramType,dataType,paramLv,paramDesc,paramRequired,paramSimpleValue) { var responseModel = new modelService.modelResponse(); responseModel.name = paramName; responseModel.paramType = paramType; responseModel.type = dataType; responseModel.lv = paramLv; if (paramDesc) { responseModel.description = paramDesc; } else { responseModel.description = ''; } if (paramRequired) { responseModel.required = paramRequired; } responseModel.simpleValue = paramSimpleValue; listDefineResponse.push(responseModel); }
5. สุดท้าย Function หลักที่ถูกเรียกหลังจาก Request Swagger แล้ว
เพื่อเตรียม data และ generate excel โดยขอแบ่งเป็น 2 ส่วน
5.1. ส่วนทำหน้าที่ Prepare Data
for (var path in json.paths) { var pathItem = json.paths[path]; for (var method in pathItem) { var itemMethod = pathItem[method]; // validate if (!itemMethod) continue; if (!itemMethod.summary) continue; if (!itemMethod.operationId) continue; var moduleName = ''; if (itemMethod.tags) { moduleName = itemMethod.tags[0]; } var serviceName = ''; if (itemMethod.operationId) { serviceName = itemMethod.operationId.replace('/',''); } else { if (itemMethod.summary) { serviceName = itemMethod.summary.replace('/',''); } } var serviceDesc = ''; if (itemMethod.description) { serviceDesc = itemMethod.description; } // validate for some module or service you want to not generate if (skipModuleForGenerate.indexOf(moduleName) > -1) continue; if (skipServiceForGenerate.indexOf(serviceName) > -1) continue; var serviceModel = new modelService.modelService(); console.log('module : ' + moduleName); serviceModel.module = moduleName; console.log('service : ' + serviceName); serviceModel.service = serviceName; serviceModel.description = serviceDesc; serviceModel.methodType = method; var param; var name; var paramType; var type; var desc; var required; /* Prepare Request */ listDefineRequest = []; for (var paramName in itemMethod.parameters) { param = itemMethod.parameters[paramName]; name = param.name; paramType = param.in; type = param.type; desc = param.description; required = false; if (param.required) { required = true; } if (param.schema) { if (param.schema.$ref) { var define = getDefineName(param.schema.$ref); var definition = getDefinitionItemRequest(json,1,define,paramType); for (var i in definition) { serviceModel.request.push(definition[i]); } listDefineRequest = []; } else if (param.schema.items.$ref) { var define = getDefineName(param.schema.items.$ref); var definition = getDefinitionItemRequest(json,1,define,paramType); for (var i in definition) { serviceModel.request.push(definition[i]); } listDefineRequest = []; } else { var define = param.schema.description; var definition = getDefinitionItemRequest(json,1,define,paramType); for (var i in definition) { serviceModel.request.push(definition[i]); } listDefineRequest = []; } } else { // LV 1 var requestModel = new modelService.modelRequest(); requestModel.name = name; requestModel.paramType = paramType; requestModel.type = type; requestModel.lv = '1'; requestModel.description = desc; requestModel.required = required; requestModel.simpleValue = ''; serviceModel.request.push(requestModel); } } /* Prepare Response */ listDefineResponse = []; for (var paramName in itemMethod.responses) { // filter for response success if (paramName != 200) continue; param = itemMethod.responses[paramName]; name = param.name; paramType = 'body'; type = param.type; desc = param.description; required = false; if (param.required) { required = true; } if (param.schema) { // LV x => for model response multi level if (param.schema.$ref) { var define = getDefineName(param.schema.$ref); var definition = getDefinitionItemResponse(json,1,define,paramType); for (var i in definition) { serviceModel.response.push(definition[i]); } listDefineResponse = []; } else if (param.schema.items.$ref) { var define = getDefineName(param.schema.items.$ref); var definition = getDefinitionItemResponse(json,1,define,paramType); for (var i in definition) { serviceModel.response.push(definition[i]); } listDefineResponse = []; } else { var define = param.schema.description; var definition = getDefinitionItemResponse(json,1,define,paramType); for (var i in definition) { serviceModel.response.push(definition[i]); } listDefineResponse = []; } } else { // LV 1 => for model response single level var responseModel = new modelService.modelService(); responseModel.name = name; responseModel.paramType = paramType; responseModel.type = type; responseModel.lv = '1'; responseModel.description = desc; responseModel.required = required; responseModel.simpleValue = ''; serviceModel.response.push(responseModel); } } serviceList.push(serviceModel); } }