setup Jacoco for an Android Multiple Module Project(Clean Architect Project,...).
Bài đăng này đã không được cập nhật trong 3 năm
Giới thiệu
Trong quá trình phát triển dự án. Việc phải tích hợp các công cụ của bên thứ ba vào một Android Project nhằm tăng tốc khả năng phát triển, đảm bảo sự ổn định, chất lượng của dự án là một việc làm cần thiết. Điều đó dẫn tới nhu cầu thiết yếu đó là biết tích hợp và cấu hình các công cụ nhằm giúp dự án thích ứng nhanh chóng với sự thay đổi liên tục từ nhiều phía như: sự thay đổi về môi trường phát triển(SDKs, libraries, server,...); sự thay đổi về yêu cầu của khách hàng; sự phân tách/ràng buộc(phân mảnh) của các chức năng trong từng giai đoạn phát triển, hoặc thâm chí là sự thay đổi nhân sự liên quan trực tiếp đến quá trình triển khai dự án, vân vân và mây mây,.... Chính vì vậy việc hiểu + kiểm soát được các công cụ thường được sử dụng trong quá trình phát triển ứng dụng Android như: Gradle Build Tool, Fastlane, Sportless, ktlint, Detekt, Jacoco, Docker,... sẽ có lợi thế rất lớn trong việc xây dựng một base code đảm bảo được những yếu tố cần thiết kể trên.
Quá trình phát triển ứng dụng ngày nay, các hệ thống CI/CD ra đời nhằm tự động hoá các công việc chân tay đơn giản nhưng tốn thời gian của developers như change environments, sync code styles, check code smells, build app, run unit tests, integration tests, deploy,.... Đặc biệt quá trình phát triển diễn ra một cách liên tục với nhiều biến thể(variants) khác nhau nên những công việc chân tay như trên lại diễn ra với tần suất ngày càng dày đặc. Chính vì vậy mà việc biết sử dụng các công cụ hỗ trợ nhằm tự động hoá những công việc kiểu này giúp developers có thể thư thái bên chén trà, cốc coffee, thảnh thơi đầu óc để đọc sách, hay thậm chí là tán gẫu, refresh sau những pha xử lý business logics phức tạp là điều đặc biệt được chú trọng.
Hôm nay mình xin giới thiệu với các bạn cách thức cấu hình Jacoco, một công cụ thường được sử dụng để đánh giá độ bao phủ của các test cases trong một dự án chạy trên môi trường JVM, trường hợp áp dụng cụ thể ở đây, tất nhiên rồi đó chính là một ứng dụng Android.
Do việc chạy integration tests đòi hỏi bạn phải thực hiện trên một môi trường Android Runtime(ART - Một Android Device, hoặc chí ít là một emulator/simulator chạy hệ điều hành Android), cũng như trong quá trình phát triển chúng ta thường focus vào việc viết + run unit tests nhằm kiểm tra các trường hợp logics + đánh giá bước đầu các rủi ro, tránh phải nhận các exceptions không mong muốn nên trong bài viết này mình sẽ chỉ tập trung vào việc cấu hình Jacoco nhằm đánh giá độ phủ(coverage) cho quá trình Unit Tests. Vì quá trình run unit test có thể được thực hiện một các dễ dàng trên một môi trường JVM thuần tuý.
Các bước thực hiện
1. Tạo và cấu hình file jacoco.gralde
Những cấu hình cần thiết để tổng hợp coverage của tất cả các modules trong project về cơ bản sẽ như sau:
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.6"
// Custom reports directory can be specfied like this:
// reportsDir = file("$buildDir/customJacocoReportDir")
}
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
}
project.afterEvaluate {
(android.hasProperty('applicationVariants')
? android.'applicationVariants'
: android.'libraryVariants').all { variant ->
def variantName = variant.name
def testTaskName = "test${variantName.capitalize()}UnitTest"
tasks.create(name: "${testTaskName}Coverage", type: JacocoReport, dependsOn: ["$testTaskName", ":YoutubePlayer:$testTaskName"]) {
// task implementation here ...
group = "Coverage"
description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build."
reports {
html.enabled = true
xml.enabled = true
xml {
destination file("${project.rootDir}/.ci-reports/coverage/coverage.xml")
}
html {
destination file("${project.rootDir}/.ci-reports/coverage/")
}
}
def excludes = [
'**/*$ViewInjector*.*',
'**/*$ViewBinder*.*',
'**/*$Lambda$*.*', // Jacoco can not handle several "$" in class name.
'**/*Module.*', // Modules for Dagger.
'**/*Dagger*.*', // Dagger auto-generated code.
'**/*MembersInjector*.*', // Dagger auto-generated code.
'**/*_Provide*Factory*.*',
'**/*_Factory.*', //Dagger auto-generated code
'**/*$*$*.*', // Anonymous classes generated by kotlin
'**/databinding/**/*.*', // Data binding
'**/R.class',
'**/R$*.class',
'**/BR.class',
'**/DataBinder*.class',
'**/*DataBinding*.*',
'**/*Directions*.*',
'**/*Args*.*',
'**/BuildConfig.*',
'**/*Adapter*.*',
'**/Manifest*.*',
'**/*Test*.*',
'**/android/**/*.*',
'**/*Fragment.*',
'**/*Fragment*.*',
'**/*Activity.*',
'**/*Activity*.*',
'**/androidx/**/*.*',
'**/com/facebook/**/*.*',
'**/fabric/*.*',
'**/CrashReportingTree.class',
'**/*Companion.class',
'**/*Kt*.*',
'**/*MapperImpl*.*',
'**/*Component*.*',
'**/*Extensions*.*',
'**/*$Result.*', /* filtering `sealed` and `data` classes */
'**/*$Result$*.*', /* filtering `sealed` and `data` classes */
]
def javaClasses = fileTree(dir: variant.javaCompiler.destinationDir, excludes: excludes)
def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: excludes)
def youtubePlayerKotlinClasses = fileTree(dir: "${project.rootDir}/YoutubePlayer/build/tmp/kotlin-classes/${variantName}", excludes: excludes)
classDirectories.from = files([javaClasses, kotlinClasses, youtubePlayerKotlinClasses])
def coverageSourceDirs = [
"$project.projectDir/src/main/java",
"$project.projectDir/src/${variantName}/java",
"$project.projectDir/src/main/kotlin",
"$project.projectDir/src/${variantName}/kotlin",
"$project.rootDir/YoutubePlayer/src/main/java",
"$project.rootDir/YoutubePlayer/src/${variantName}/java",
"$project.rootDir/YoutubePlayer/src/main/kotlin",
"$project.rootDir/YoutubePlayer/src/${variantName}/kotlin"
]
additionalClassDirs.from = files(coverageSourceDirs)
sourceDirectories.from = files(coverageSourceDirs)
executionData.from = files(["${project.buildDir}/jacoco/${testTaskName}.exec",
"${project.rootDir}/YoutubePlayer/build/jacoco/${testTaskName}.exec"])
}
}
}
task copyMd(type: Copy) {
android.applicationVariants.all { variant ->
def variantName = variant.name
def testTaskName = "test${variantName.capitalize()}UnitTest"
from "${project.buildDir}/reports/tests/$testTaskName"
into "${project.rootDir}/.ci-reports/summary"
}
}
(cwl) (cuoikhoc) Nhìn thì có vẻ hơi lộn xộn phải ko nào? Cơ mà mình sẽ giải thích chi tiết từng phần.
1. Lấy variantName và thiết lập testTaskName
Mỗi dự án Android thông thường có 2 variants mặc định là delopve và release. Tuy nhiên trong quá trình phát triển chúng ta có thể tạo ra thêm nhiều variants khác cho những mục đích riêng. Tuỳ từng mục đích mà Android Developer có thể thiết lập thêm các variants khác nhau từ việc đơn giản nhằm thay đổi môi trường phát triển phía server(develop, staging, production,...) hay thậm chí là thay đổi cả mã nguồn(Mỗi variant có libraries riêng, có mã nguồn riêng). Điều này làm cho việc viết Unit Tests trên mỗi variant là khác nhau dẫn đến việc Jacoco tính toán coverage cho mỗi variant cũng là khác nhau.
Đoạn mã lấy variantName để tạo các task tính coverage tương ứng như bên dưới:
(android.hasProperty('applicationVariants')
? android.'applicationVariants'
: android.'libraryVariants').all { variant ->
def variantName = variant.name
def testTaskName = "test${variantName.capitalize()}UnitTest"
}
Ở đây đơn giản chỉ là ứng với mỗi variant chúng ta mặc định sẽ có một task run unit test tương ứng đó là: "test${variantName.capitalize()}UnitTest"
Chính điều này đặt ra một bài toán đó là chúng ta sẽ phải đồng bộ variant cho tất cả các module trong dự án của mình nhé. Đừng để module A có variant A,B,C mà module B lại có các variant khác là X,Y,Z nhé.
2. Tạo gradle task.
Tiếp theo chúng ta sẽ tạo ra task tính coverage cho mỗi testTaskName tương ứng bằng đoạn mã dưới đây:
tasks.create(name: "${testTaskName}Coverage", type: JacocoReport, dependsOn: ["$testTaskName", ":YoutubePlayer:$testTaskName"]) {
// task implementation here ...
group = "Coverage"
description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build."
reports {
html.enabled = true
xml.enabled = true
xml {
destination file("${project.rootDir}/.ci-reports/coverage/coverage.xml")
}
html {
destination file("${project.rootDir}/.ci-reports/coverage/")
}
}
...
}
Một số cấu hình cần chú ý ở đây đó là:
- task name: "${testTaskName}Coverage" => Điều này có nghĩa với mỗi variant chúng ta sẽ có một testTaskName, và mỗi testTaskName sẽ có tương ứng một testTaskNameCoverage để tính coverage bằng Jacoco.
- type: JacocoReport. Dĩ nhiên rồi. =))
- dependsOn: Chúng ta cần focus vào phần này. Ở đây chúng ta cần liệt kê tất cả các modules mà mình muốn combine coverage vào trong report. Trong cấu hình của mình hiện tại đang có 2 modules đó là: app và YoutubePlayer. Do app là application variant nên chúng ta chỉ cần thêm vào "$testTaskName". Ngược lại các library variants chúng ta sẽ cần thêm tiền tố tương ứng. Ở đây là: ":YoutubePlayer:$testTaskName". Như vậy đối với Clean Architect chúng ta sẽ cần cấu hình là: ":data:$testTaskName", ":domain:$testTaskName",... cho các modules data, domain,... tương ứng.
- Ngoài ra là phần cấu hình: Group nhóm các gradle tasks này vào một group để dễ quản lý hay description để mô tả chi tiết cho gradle group tasks.
- Cuối cùng là thiết lập report cho Jacoco. Phần này là option. Thường là tiện cho chúng ta xem report, hoặc là nơi để adapt với các hệ thống CI/CD.
3. include/exclude classes.
Theo kì vọng thì chúng ta sẽ phải tính coverage của unit tests cho toàn bộ mã nguồn trong project của mình. Tuy nhiên, không phải đoạn mã nguồn nào chúng ta cũng cần/có thể viết unit tests, do đó chúng ta cần cấu hình để include/exclude những phần tương ứng cho kết quả combine của Jacoco.
Trong đoạn cấu hình bên trên, phần include, mình đã load tất cả các mã nguồn java + kotlin từ 2 modules(app và YoutubePlayer) rồi thiết lập cho thuộc tính classDirectories của Jacoco.
def javaClasses = fileTree(dir: variant.javaCompiler.destinationDir, excludes: excludes)
def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: excludes)
def youtubePlayerKotlinClasses = fileTree(dir: "${project.rootDir}/YoutubePlayer/build/tmp/kotlin-classes/${variantName}", excludes: excludes)
classDirectories.from = files([javaClasses, kotlinClasses, youtubePlayerKotlinClasses])
4. setup coverage source directories.
Phần này là thiết lập source directories để các bạn có thể tham chiếu tới mã nguồn của các methods từ các methods được Jacoco count. Điều này là cần thiết bởi vì bạn sẽ cần phải xem chi tiết độ phủ của unit tests trên từng phương thức để viết hết các case logic tương ứng.
def coverageSourceDirs = [
"$project.projectDir/src/main/java",
"$project.projectDir/src/${variantName}/java",
"$project.projectDir/src/main/kotlin",
"$project.projectDir/src/${variantName}/kotlin",
"$project.rootDir/YoutubePlayer/src/main/java",
"$project.rootDir/YoutubePlayer/src/${variantName}/java",
"$project.rootDir/YoutubePlayer/src/main/kotlin",
"$project.rootDir/YoutubePlayer/src/${variantName}/kotlin"
]
additionalClassDirs.from = files(coverageSourceDirs)
sourceDirectories.from = files(coverageSourceDirs)
Ví dụ từ kết quả count coverage của SampleViewModel, chúng ta cần xem chi tiết xem những phần nào đã được viết cover bằng Unit Tests, những phần nào chưa.
Và khi bấm vào xem chi tiết:
Ở phần source này các bạn cần chú ý liệt kê đầy đủ các đường dẫn vào mã nguồn để Jacoco thiết lập tham chiếu đầy đủ + chính xác cho mã nguồn của mình nhé.
2. apply jacoco cho tất cả subproject(multiple modules).
Do project của chúng ta có nhiều module mã nguồn khác nhau, nên chúng ta phải apply Jacoco cho tất cả các module này. Điều này là đặc biệt cần thiết. Vì đối với mỗi module, khi build gradle, Jacoco sẽ generate ra một file execution dạng testDebugUnitTest.exec. Nếu các bạn để ý thì các files này phải được load(thiết lập) đầy đủ cho thuộc tính executionData của Jacoco. Điều này là đặc biệt cần thiết vì nó liên quan trực tiếp đến kết quả tính coverage cuối cùng.
Để Jacoco generates ra được các file này cho toàn bộ các modules, chúng ta đơn giản chỉ cần apply jacoco cho tất cả các subproject trong file build.gradle tại thư mục root của project.
subprojects {
apply { plugin("jacoco") }
}
3. setup trong build.gradle.
Phần còn lại là apply jacoco và include file cấu hình trên vào app module. Công việc đơn giản chỉ là thêm đoạn dưới vào build.gradle của app module.
plugins {
id("jacoco")
}
apply {
from("../buildSrc/jacoco.gradle")
}
Cũng như nên tắt tính năng tính coverage tự động đi bằng cách thiết lập dòng dưới cho các build type để tránh cách impacts có thể nhé(Vì cái này tự động tính coverage cho cả integration tests):
isTestCoverageEnabled = false
Như vậy đến đây chúng ta đã hoàn thành các bước cơ bản việc cấu hình Jacoco cho một project của nhiều modules. Kết quả các bạn sẽ thấy coverage được combine vào thành một report duy nhất.
4. other settings.
Phần này mình sẽ đề cập tới một số cấu hình khác, cái sẽ cần thiết trong quá trình làm dự án thực tế như: Làm thế nào để ignore các classes, các methods,. Những phương thức này thường là những phương thức mà các đối tượng được sử dụng trong đó rất khó để mock(Ví dụ như FirebaseCrashlytic - Cái mà thỉnh thoảng chúng ta cần để có được thông tin warning cho những trường hợp logic nguy hiểm, những case logic phát sinh độ rủi ro cao). Điều này là thực sự cần thiết vì tất nhiên rồi. Nó ảnh hưởng trực tiếp đến kết quả tính toán coverage của chúng ta mà.
Excludes classes
Phần này ngoài các cấu việc cấu hình để ignore các classes trong các thư viện cũng như các class được gen ra trong quá trình build, các bạn cần chú ý đến một số file extension của kotlin. Thường các file chứa các extension methods chúng ta không wrapp nó vào trong một class cụ thể nào để dễ dàng call ở bất cứ nơi đâu giống như những static methods toàn cục. Tuy nhiên, khi compile ra Bytecode thì những methods này được tự động đóng gói vào một class được đặt theo tên file kotlin kèm theo hậu tố "Kt". Do đó khi excludes các extension classes này chúng ta cần thêm hậu tố trên vào. Ví dụ file kotlin của chúng ta là Extensions thì chúng ta sẽ phải exclude file có dạng ExtensionsKt. Hoặc các bạn cũng có thể chỉ định tên file này bằng cách đặt annotation: @file:JvmName("Extensions"). Điều này sẽ cho JVM biết cần phải compile file này với class name là Extensions. Và do đó trong lúc cầu hình chúng ta sẽ ko cần phải quan tâm đến hậu tố "Kt" kia nữa.
Excludes methods
Không phải lúc nào chúng ta cũng cần ignore toàn bộ mã nguồn trong một class. Đôi khi chúng ta chỉ cần ignore một hoặc một vài methods trong đó mà thôi. Vậy thì phải làm thế nào. Để thực hiện được việc này, từ version 0.8.6 trở đi, Jacoco tự động hỗ trợ việc ignore các file được đánh annotation bằng chuỗi kí tự có chứa chuỗi generated do đó chúng ta có thể tự custom cho mình một annotation như thế. Tuy nhiên, Jacoco được build dựa trên một công cụ tuyệt vời khác đó chính là lombok. Để hiểu thêm lombok là gì và cách sử dụng một cách chi tiết các bạn có thể tham khảo ở đây. Trong phạm vi bài viết này mình chỉ xin giới thiệu qua cách cấu hình + áp dụng để ignore một methods, classes,... bằng lombok trong Jacoco report mà thôi.
1. Thêm lombok dependency vào project Để thiết lập lombok cho Android các bạn có thể tham khảo chi tiết ở đây. Những thiết lập này cụ thể gồm 2 bước: Bước 1: Thêm lombok plugin vào Android Studio. Bước 2: Thêm lombok dependencies vào gradle.
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.16'
annotationProcessor 'org.projectlombok:lombok:1.18.16'
}
Chi tiết hai bước này các bạn có thể refer tài liệu trên trang chính thức để biết thêm.
2. Thêm lombok configuration Thêm file lombok.config vào thư mục gốc của dự án với nội dung bên dưới:
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true
- config.stopBubbling = true: nói cho lombok biết rằng đây chính là thư mục gốc của project, do đó không cần phải tìm kiếm các thư mục cha để load các files cấu hình khác.
- lombok.addLombokGeneratedAnnotation = true: Nói cho lombok biết nó cần thêm @lombok.Generated annotation vào tất cả các phương thức được sinh ra. Điều này là cần thiết. Bởi vì chúng ta sẽ có rất nhiều methods được sinh ra trong quá trình compile như equal(), hashCode(), toString(), component(),.... Đây là những phương thức rõ ràng là chúng ta không cần phải viết unit tests cũng như tính coverage cho chúng.
3. Thêm @lombok.Generated vào methods/classes muốn ignore Đến đây. Với mỗi class/method cần ignore chúng ta chỉ cần thêm @lombok.Generated annotation cho chúng. Các bạn có thể tự thử và xem kết quả.
Setting Gradle Cache
Đây chỉ được coi như một phần bonus. Là phần mà bạn sẽ quan tâm nếu muốn tích hợp dự án của mình với một hệ thống CI/CD có sẵn. Trong quá trình build trên CI/CD system, một vấn đề vô cùng quan trọng đó là phải cache lại kết quả build để tái sử dụng cho những lần build sau. Tránh mỗi bản build phải build đi build lại nhiều lần cả những đoạn code không có sự thay đổi. Điều này tránh việc phí phạm tài nguyên, cũng như thời gian chờ đợi cho developers.
Mặc định, dữ liệu build source code của bạn sẽ được gradle tự động cache lại ở nơi nó được cài đặt(hoặc ở GRADLE_HOME nếu bạn đã từng thử install + config bằng tay). Cái này đối với Android Developers chúng ta thường hay không quan tâm lắm. Bởi vì anh em cứ cài Android Studio xong, chẳng cần JAVA_HOME, ANDROID_HOME, GRADLE_HOME,... là đã có thể code túi bụi rồi đúng ko? =))) Tuy nhiên, trên các hệ thống CI/CD, môi trường build của bạn sẽ liên tục bị clear và khởi tạo lại để tránh việc tốn kém tài nguyên hệ thống. Tức là nó chỉ cache lại container chứa môi trường bản build mà không cache toàn bộ container cùng với kết quả build của các lần build trước đó. Thêm nữa, mỗi lần build, có thể môi trường build sẽ có những thay đổi theo cấu hình mới trong dự án, do đó nếu cache toàn bộ container đó sẽ là một giải pháp tồi, gây ra sự lãng phí không hề nhỏ. Chính vì vậy, ở đây chúng ta cần cấu hình gradle để save cache lại trong project của chúng ta, rồi thiết lập để CI/CD lưu lại các content này. Để cấu hình lại cache của gradle đơn giản chúng ta thêm đoạn mã nguồn sau vào file settings.gradle trong project của mình.
buildCache {
local {
// Set local build cache directory.
directory = "${settingsDir}/build-caches"
}
}
Rồi sau đó thêm vào phần cấu hình cache của CI/CD là xong rồi.
cache:
- key: BUILD_CACHES
paths:
- ./.gradle
- ./build
- ./app/build
- ./build-caches
Kết luận
Chỉ với việc vọc thêm cấu hình Jacoco chúng ta đã có thể hiểu thêm được một vài công cụ hỗ trợ quá trình phát triển ứng dụng Android, cũng như biết thêm một vài công cụ mới rồi đúng không? Hy vọng bài viết giúp ích cho các bạn trong quá trình thiết lập Jacoco nhằm tính toán chính xác coverage cho dự án của mình, cũng như có được những dự án với coverage lung linh, long lanh nhằm show cho các managers + khách hàng của mình nhé. Cơ mà lunh linh, long lanh cũng vừa phải và hợp lý thôi nhé. Unit Tests + Coverage chỉ là một công cụ giúp tăng cường chất lượng mã nguồn của dự án chứ không phải tất cả đúng không? Quan trọng vẫn là business logic phải đúng + ít bugs nhé.
Meditation =)))
Phần này chỉ là quan điểm cá nhân của mình thôi, nên mới đặt là Meditation nhé. Các bạn có thể tham khảo, hoặc không. =)))
1. Viết Unit Test + tính coverage chỉ là chạy qua hết các dòng lệnh, và vì thế khi viết Unit Test sau khi đã implement source code là vô nghĩa.
Điều này không hẳn sai, cơ mà cũng không hoàn toàn đúng đâu nhé. Việc viết Unit Tests dù trước, hay sau đều có ý nghĩa. Sau đây mình xin nêu ra 2 lý do mà dù viết sau thì công việc này vẫn mang tới nhiều ý nghĩa tích cực nhé.
- Viết Unit Tests sau quá trình implement source code giúp bạn review lại chính mã nguồn của mình. Trong lúc viết Unit Tests bạn sẽ nhận ra trong source code của mình hình như có cái gì sai sai, hoặc thiêu thiếu cái gì đó đấy nhé. Chắc chắn một điều là chúng ta không bao giờ hiểu + xử lý các business logic cases một lần là đúng luôn đúng không nào?
- Một lợi thế khác nữa là khi có change requests, dựa vào unit tests + coverage cũ chúng ta có thể một phần nào đó đánh giá được mức độ tác động của những thay đổi trong chính function của mình. Thâm chí là impact sang các function khác. Điều này đặc biệt hữu ích vì các bạn biết rồi đấy, có những impacted cases không hề dễ nhận ra => Tỉ lệ xảy ra lỗi + tái hiện bugs là rất nhỏ. Nhưng một khi xảy ra incidents lại là một chuyện không hề nhỏ đúng không?
2. Unit Tests + Coverage cao không phải là tất cả.
Với những gì mình đã trình bày ở trên + một số lần thử viết Unit Test, tính Coverage thì các bạn cũng rút ra được kết luận là mọi con số đều có thể chỉnh sửa được rồi đúng không? Mình rất thích tính cách của anh em dân kĩ thuật đó là Thô nhưng mà thật. =))) Chính vì vậy bạn biết là mình luôn tin ở chính các bạn rồi đấy đúng không? =)))
Ở đây mình chỉ xin nhấn mạnh lại một lần nữa: Unit Tests + Coverage cao không phải là tất cả. Okay.
3. Beginner's Mind - Tâm thế của người mới bắt đầu.
Một khái niệm khá thú vị. Chắc hẳn đã có nhiều bạn biết rồi. Tuy nhiên mình vẫn nên ra ở đây. Coi như là một sự suy ngẫm về con đường sự nghiệp. Đặc biệt là con đường mà những kẻ tay mơ như mình và các bạn đang đi qua.
Người mới bắt đầu thì ko mang theo thành kiến.
Đặc điểm thú vị đầu tiên của người mới bắt đầu chính là không mang theo thành kiến. Người mới bắt đầu đóng vai trò như một người quan sát, lùi lại để ngắm nghía, phân tích sự vật như những gì nó vốn có. Không biết các bạn đã bao giờ nghe câu: Chính những điều mình biết là thứ cản trở khả năng phát triển của bản thân mình. Điều này càng nghĩ lại càng thấy thấm thía. VD: Khi tiếp xúc với một kiến trúc mới như MVP, MVVM, Clean Architecture, VIPER,... hay một công nghệ mới như React Native, Flutter,... mà trong tâm bạn có suy nghĩ mình đã làm nhiều rồi, mình hiểu rồi thì chắc chắn một điều bạn đã đóng cánh cửa tiếp thu cái hay, cái mới của bạn thân mình lại rồi. Vì đơn giản, cùng là một kiến trúc, một công nghệ nhưng đối với những trường hợp thực tế khác nhau, các điều kiện cũng như yêu cầu khác nhau sẽ ra vô vàn các biến thể khác nhau. Đừng bjo nghĩ một kiến trúc, hay một công nghệ là đúng trong tất cả mọi trường hợp. Quan trọng là ở người sử dụng.
Thực tế trong quá trình làm việc, mình thấy nhiều anh em khá cứng nhắc trong việc lựa chọn công nghệ + giải pháp. Rồi tranh luận tùm lum, nhất quyết phải làm theo cách của mình mới đúng. Xong rồi đến lúc implement thì ra đầy bugs. (cwl) Kiến trúc tốt là điều đáng để triển khai + theo đuổi. Cơ mà nhiều business logic cases lại chẳng chú tâm mà take care cho hết. Mà những cases đó mới là cases gây bực tức cho khách hàng. Unit Test để cố gắng cover hết những cases như thế. Thế nên mình nghĩ ko có giải pháp hoàn hảo cho tất cả mọi vấn đề. Bạn sài cái gì cũng ko quá quan trọng. Đừng lởm quá là được. Và cover hết các business logics cần thiết. Có một sản phẩm ngon. Đầy logic cases mà ae xử lý rối mù, sửa flag A thì impacts tới cases B. haha Tha hồ fix bugs. Hoặc một kiến trúc phải implement lòng vòng một xíu. Đánh đổi thời gian implementation để đảm bảo chất lượng chẳng hạn. Đấy cũng là một điều đáng để làm + thử. Biết đâu khi practice ngon rồi. Biết được chỗ nào có thể tự động generate code được, lại làm ra quả tools thần thánh thì sao?
Mình luôn tin rằng sự chăm chỉ làm việc + không ngừng học hỏi chính là điều quan trọng nhất giúp bạn tiến về phía trước và giúp bạn mỗi ngày một mới, một hay, và ngày càng càng mới hơn, hay hơn. =)))
Quote: "Không có kiếm pháp vô địch, chỉ có những con người vô địch; không có nghề cao quý, chỉ có những con người cao quý".
Người mới bắt đầu luôn hạ thấp cái tôi cá nhân.
Vì mong muốn học hỏi, và vì bắt đầu ở con số không, nên tất nhiên rồi họ(beginners) sẽ phải hạ thấp cái tôi cá nhân của bản thân. Để được trao đổi, tìm tòi, học hỏi,... cùng mọi người. Cái tôi cá nhân quá cao thực sự là một trở ngại rất lớn trong quá trình học hỏi. Đành rằng cái tôi cá nhân cao, bạn có thể ngăn hết những thứ xấu xa, độc hại từ bên ngoài. Nhưng nó cũng vô hình chung làm bạn mất đi cơ hội. Giống như việc một nước bế quan tỏa cảng, ko tiếp thu nền văn minh từ bên ngoài ấy. Đặc biệt đối với newbie, beginners thì "Cái tôi cá nhân" cao lại càng là vật cản lớn. Đừng để cảm xúc cá nhân, hay định kiến có sẵn trong mình mà bỏ qua những cơ hội học hỏi + làm một cái gì đó mới, khác với những thứ mà bản thân mình vẫn làm.
Người mới bắt đầu tôn trọng sự thật.
Đa số người làm kĩ thuật(giống chúng ta) được xã hội đánh giá thuộc kiểu người: Thô mà thật. Tất nhiên là mình không nói tất cả nhé, vẫn có những ông thật thà mà tinh tế như cái ông đọc bài viết đến tận đây chẳng hạn. =))) Tuy nhiên ở đây mình muốn đề cập tới một vấn đề. Khi mà người ta bị mâu thuẫn lợi ích, người ta lại đánh mất bản ngã của con người mình. Đa phần những ông sếp dở đều thích những đứa hay diễn, nói hay, hát giỏi. Và cứ thế tạo ra một trào lưu diễn, một tiền lệ xấu trong môi trường làm việc. Ông coder thì diễn cho ông Team Leaders, ông Team Leaders thì diễn cho ông Managers, ông Managers thì diễn cho ông nào nhỉ ???. Nói chung diễn cho cuộc đời đẹp hơn, tinh tế hơn cũng chẳng vấn đề gì. Làm hài lòng được tất cả mọi người thì tuyệt vời quá đi còn gì nữa.
Nhưng ở đây mình muốn nhấn mạnh là "đánh mất bản ngã". Thực sự trong quá trình làm việc mình quan sát thấy khá nhiều trường hợp hơi buồn như vậy.
Ví như có cậu, nói gì nó cũng vâng, bảo gì nó cũng dạ bất kể đúng/sai, phải/trái. (cwl) Miễn là sếp bảo. Lúc nào nó cũng muốn hình ảnh bản thân mình lung linh nhất trong mắt các sếp. =)))
Hoặc nhiều ông, anh em trong team, tình hình dự án thì đéo chịu quan tâm, mà cứ mải mê đi diễn ở tận đâu đâu(Chắc có ông sếp nào to hơn cần nịnh nọt, hoặc có việc cá nhân gì cần phải để ý). (cwl) (cuoikhoc) Xong rồi phán những câu xanh rờn kiểu như: "Trong dự án ai anh em nào chẳng làm tốt", "Dự án anh lead chưa bjo fail",.... (cwl) Rồi thỉnh thoảng nhớ tới mấy cái issues trong dự án thì cũng hỏi han, chỉ dẫn mà mỗi tội mấy cái issues đó đã out of date từ mấy hôm trước. Trong khi ngày nào cũng có daily report. (cwl) (cuoikhoc).
Team tan cmnr mà vẫn hùng dũng, hiên ngang: "Tầm quan trọng của teamwork", "we are one team",.... hỜ hỜ Chắc ở một nơi nào đó người ta thích những người nói: "We are one team",... chứ ở trong team thực tế thì méo cần nhé. (cwl) (cuoikhoc).
Hoặc có nhiều ông biết khả năng bản thân có hạn, không chịu thay đổi, tích lũy thêm kinh nghiệm lại thao thao bất tuyệt join vào những nhóm lợi ích để mà bảo vệ lẫn nhau, kiểu cty là nhà, coworkers là anh chị em. Làm cho môi trường làm việc chia năm sẻ bảy, rồi nhóm nọ đấu đá, nói xấu nhóm kia. Việc kiến tạo sản phẩm ngon thì méo làm, suốt ngày chỉ chăm chăm nghĩ ra mấy cái mưu hèn, kế bẩn để mà nói xấu, công kích các đối tượng còn lại. Thử hỏi như thế thì lấy gì để có một VN hùng cường đây? Lấy gì để mà so sánh với Nhật, với Hàn, hay thâm chí là với mấy ông trong ao làng(Singapore, Thailand, Philippine, Malaysia,...) đây??? (cwl) (cuoikhoc).
Mình thì mình tin ai làm thực, có kinh nghiệm thực tế sẽ có khả năng validate sự thật. =)) Vì người từng làm sẽ hiểu nắm được chi tiết, biết chỗ nào thừa, chỗ nào thiếu để mà làm cho công việc trở nên tinh gọn. Như thế chỉ cần hỏi chi tiết một xíu thôi là mọi thứ bị lột trần trụi thôi à. Bạn lie với mấy ông làm rồi thì chỉ làm ra mấy câu chuyện cười hài hước thôi. Định múa rìu qua mắt thợ à. Chém !!! =)))
Thêm nữa lý thuyết thì có thể nói đông nói tây, chứ kĩ năng chẳng bao giờ có được trong chốc lát. Ông nào cũng có thể nói lý thuyết tốt nhưng thực tế dự án thì ông coders khác, ông developers khác, ông programmer khác,.... Nhìn khả năng tập trung xử lý vấn đề + cách mà một member đối mặt với sự rảnh rỗi là đủ để biết khả năng của người đó như thế nào rồi. Hay như nhìn vào khả năng copy + implementation codes. Ông coder sẽ bảo: "Sao copy y hệt mà lại ko chạy". Ông Dev sẽ ngay lập tức xem xét sự giống khác nhau, ông programmer thì đã có cách để copy đỡ sai cmnr. Méo phải lúc nào lên google cái copy => paste là cũng giải quyết được vấn đề đâu???
Người mới bắt đầu ham học hỏi(Hợp tác 100%).
Hợp tác và học hỏi là kĩ năng cần thiết để phát triển trong thời đại này rồi. Đặc biệt là những con người ham danh vọng như phần đông chúng ta. Tuy nhiên sự ham học hỏi, tinh thần hợp tác chưa chắc đã được thể hiện đúng. Vì mẫu thuẫn lợi ích hoặc vì thành kiến quá nặng chẳng hạn. Cũng như việc nhiều ông lúc nào cũng chăm chăm mình phải có lợi hơn thì mới chơi. Cái mà người ta bảo là: Khôn như ông quê tôi đầy ấy. Ông nào cũng tranh khôn, thì ai là người dại đây? =))) "Thánh nhân đãi kẻ khù khờ cơ mà"? =)))
Ngoài ra mình cũng xin được đề cập tới hai tác phẩm khá hay để bạn nào nếu chưa đọc, cũng nên tham khảo một lần để trang bị cho mình mindset ok hơn nhé. Đó là: "7 thói quen của bạn trẻ thành đạt" và "Người giỏi không phải là người làm tất cả".
Quyển "7 thói quen..." trang bị cho các bạn mindset về việc muốn hạnh phúc thì phải độc lập. Giống như câu mà ai ai trong chúng ta cũng đặt trên bàn thờ tổ quốc: "Độc lập - Tự do - Hạnh phúc" ấy. Có độc lập thì mới tự do, có tự do mới mưu cầu được hạnh phúc. Tuy nhiên, chúng ta không thể tồn tại một mình, và lấy đó làm hạnh phúc được, mà chúng ta phải sống tương thuộc(Giao kết, hợp tác với người khác). Mà muốn tương thuộc thành công, thì phải hợp tác. Mà muốn hợp tác thành công, lâu dài thì phải đôi bên cùng có lợi. =)))
Quyển "Người giỏi không phải là người làm tất cả" thì lại trang bị cho các bạn kĩ năng tuy đơn giản nhưng lại cần thiết trong việc chia sẻ công việc(Vì ko phải tự tay chúng ta có thể làm tất cả mọi việc đúng ko?). Trong sách có một phần khá thú vị đó là: Tầm quan trọng của việc xác định thẩm quyền. Đại loại là hợp tác với người khác mình phải xác định xem mình là ai, và mình ở đâu. Ví dụ, hợp tác với người mới, ít kinh nghiệm hơn thì mình cần phải hướng dẫn, chỉ bảo. Tuy nhiên với người có khả năng tương tự mình thì chỉ nên đưa ra đề xuất, đề nghị; còn người có khả năng tốt hơn mình thì tốt hơn là trao quyền, tất nhiên trao quyền có giám sát, cơ mà thỉnh thoảng hỏi han để nắm bắt tình hình khác hẳn với việc giám sát từ đường đi nước bước hoặc chỉ hỏi cho biết để report cho một ông sếp to hơn đấy nhé. (cwl) (cuoikhoc). Nói thế để có thể thấy việc hợp tác là ko đơn giản phải không nào. Với người có khả năng giống mình, hoặc cao hơn mình mà cứ chăm chăm đưa ra chỉ dẫn. Chắc chỉ có mấy ông vua thời phong kiến làm được thôi. (cwl) (cuoikhoc). À mà quên: "Người giỏi không phải là người làm tất cả" nhưng mà "Không phải là không làm gì" nữa nhé. haha
Người mới bắt đầu nghi ngờ tất cả.
Ở đây nghi ngờ tất cả không phải là không có niềm tin vào người khác, niềm tin vào anh em, đồng đội. Mà là nghi ngờ ngay cả chính những thứ mà bản thân đã biết, đã chắc chắn, nghi ngờ cái mà anh em, đồng đội nó quả quyết với mình. Để từ đó làm hoàn chỉnh vấn đề. Chứ nhiều ông. Không biết rõ, không biết chắc mà phán như đúng rồi. Mang mấy cái thuật ngữ ít người biết ra đề mà hù, mang mấy cái từ nghe vẻ màu mè, kiểu cách nhưng lại chẳng có ý nghĩa để mà tăng thêm sự hoành tráng cho lời nói của bản thân(Đi trà đá chém gió mua vui có ai lại nói là đi đàm, đi luận trà đá không mọi người)?.
Ví dụ: "Coverage không tính tổng được cho cả project nhưng có thể tính riêng cho từng layers, hiện tại CI đang áp dụng jacoco. Cái invoke đề cập đến chủ yếu trên view (presentation layer) phần này sử dụng Android Instrumentation Test, phần này nó ko đáng kể gì so với tổng thể của architecture. Với clean data flow, error flow qua rx stream ko thông qua interface nên ko có invoke." =))) Các bạn nếu đã làm + hiểu về viết unit test + tính coverage ở đây, sẽ thấy đoạn trên toàn là suy luận. Thứ nhất, chữ không thể hiện một cách chắc chắn và thực tế thì không phải vậy. Thứ hai trong kotlin, bạn hoàn toàn có thể override lại hàm invoke để lấy một data type theo một logic hoàn toàn khác. Thậm chí đổi trắng thay đen, thay vì trả về 0, bạn trả về 1, thay vì trả về nam, bạn trả về nữ. Và nếu là logic đó, chúng ta có nên viết unit test cho nó hay không? Thứ hai, bất cứ data type nào cũng có thể viết lại hàm invoke, không cứ ở presentation layer. Cái này ở đâu, thì tác giả của đoạn trên mới giải thích được. haha . Lại còn Android Instrumentation Test nữa chứ. Toàn mang mấy cái thuật ngữ ra để hù người thôi.
var user = UserObject(displayName = "The Truthful Person")
operator fun UserObject.invoke(): UserObject {
return UserObject(displayName = "The Liar")
}
=> Bạn thử gọi invoke xem. Có đổi trắng thay đen ko? (cwl) (cuoikhoc)
Phần nữa, "Với clean data flow, error flow qua rx stream ko thông qua interface nên ko có invoke." đố các bạn hiểu là đang nói gì đấy. Ko thông qua interface thì ko có invoke? Tức là chỉ interface mới có invoke method, hay là data object bình thường ko có invoke method?
Tôi thực sự không hiểu tại sao người ta lại có tài vẽ mây vẽ gió không có thực tế như thế các ông ạ. (cwl) (cuoikhoc)
Rồi nhé: "Theo Clean (số 1 vẫn là testable) nên nếu để test coverage cao nhất chắc Clean vẫn là số 1, viết code chuẩn hoàn toàn có thể coverage được 60-70% (Unit Test), thêm 1 chút integration test sẽ lên được 85%, thêm một chút Automatic Test hoàn toàn được 90%." haha .
Các bạn thấy toàn thuật ngữ kĩ thuật thần thánh chưa? Các bạn đã đủ thấy sợ chưa? Anh em làm unit test, đã bjo thử mock resource để lấy một json string từ file thông qua input/output stream chưa? (cwl) (cuoikhoc) Chắc chỗ này ổng kia ông ý phải lấy Automatic Test thì mới có thể tăng coverage lên 90% thôi. haha .
Tôi chỉ khuyên các bạn một câu. Đừng nghe người ta nói. Hãy xem cái cách người ta làm. Xem các cách người ta triển khai jacoco để có unit test, integration test, automatic test và đạt coverage 90%. Hoặc không nói nhiều, đưa sản phẩm ra xem thử. Xem có cứng họng luôn không? =)))))))
Quote: "Nếu có ai quả quyết với mình một điều gì, phải lập tức hỏi ngay bằng cứ"!
Còn bản thân mình vẫn luôn tin rằng: Người làm thực, có kinh nghiệm sẽ là người có khả năng validate sự thật. Người ta thấy bạn nói thế có thể người ta sẽ không nói gì đâu, người ta chỉ tự cười thầm trong bụng mà thôi. =)))
4. Mông lung trên con đường sự nghiệp.
Chắc chắn sẽ có khoảng thời gian nào đó, đối mặt với con đường sự nghiệp mình đã chọn bạn sẽ có cảm giác mình lạc lõng. Ngay cả bản thân mình cũng thế thôi. Tuy nhiên mình vẫn tin là nói thật, làm thật sẽ là ok nhất. Hoặc nếu không nói được hãy chọn sự im lặng, chứ đừng nói dối. Okay? Bạn sẽ được sống một cuộc sống thật là mình nhất. Mặc dù tôn trọng sự thật là một việc làm siêu khó. (cwl) (cuoikhoc). Nhưng Keep It Goin'. Có thể sự nghiệp của bạn gặp nhiều may mắn, thăng tiến liên tục, không gặp cản trở gì, hoặc bạn là một người dễ chấp nhận, an phận thì bạn sẽ không phải trải qua cảm giác bước đi trên con đường tìm kiểm một môi trường làm việc phù hợp(nhảy việc liên tục). Tuy nhiên mình nghĩ đó cũng là một cảm giác không phải dễ chịu đâu các bạn ạ. Qua mỗi công ty là để lại một mảnh thanh xuân đấy bạn ạ. (cwl) (cuoikhoc).
Bạn sẽ phải mất thời gian để adapt với công việc mới, mật thời gian để chứng minh năng lực của bản thân. Cơ mà mình nghĩ đừng vì thế mà từ bỏ bạn nhé. Phần thưởng cuối cùng chỉ dành cho những người đủ kiên nhẫn theo đuổi tới cùng.
Mỗi người một số phận, nên chắc chắn chúng ta không thể có những cảm nhận giống nhau được đúng không?
Nếu rảnh rỗi, hãy học cho mình thêm một kĩ năng gì đó mới(đọc sách, chơi nhạc, tìm hiểu công nghệ, các nền văn hóa khác,...). Nó không hẳn sẽ giúp ích cho bạn trên con đường sự nghiệp nhưng chắc chắn sẽ giúp bạn bước đi trên con đường đó được lâu dài hơn nhé.
Nhân tiện cũng giới thiệu thêm quyển sách: "Một đời thương thuyết - Phan Văn Trường" để các bạn có cái nhìn ở mặt đất cho vấn đề sự nghiệp, cũng như vững tin hơn trên con đường sự nghiệp của bản thân mình nhé.
P/S: Trong suốt thời gian đi làm và trôi dạt các cty của mình. Thật may mắn khi mình luôn được anh em bạn bè quý mến(Ngay cả khi mình hoặc họ đã chuyển cty khác). Thậm chí cũng may mắn khi được làm việc cùng với những PMs, Teams cực kì tử tế. Họ luôn chỉ dẫn, đối xử một cách tử tế(như một partner, không phân biệt, kênh kiệu) ngay cả khi biết rằng mình, và team VN của mình lởm ntn. (cwl) (cuoikhoc) Và một trong những câu quote truyền cảm hứng mà mình học được từ họ đó là: "I'm not perfect, but I could be better". =)))
Hy vọng nó cũng có thể một phần truyền cảm hứng cho bạn.
{>;} Happy Coding!
All rights reserved