1. Xử lý File trong Java
Đọc và ghi file trong java là các hoạt động nhập/xuất dữ liệu (nhập dữ liệu từ bàn phím, đọc dữ liệu từ file, ghi dữ liệu lên màn hình, ghi ra file, ghi ra đĩa, ghi ra máy in…) đều được gọi là luồng (stream).
1.1 Một số class thường sử dụng
File
: Là thực thể dẫn tới file, hoặc thư mục, cho các hàm để làm việc với FileFileInputStream/FileOutputStream
: Đọc ghi file binaryFileReader/FileWriter
: Đọc ghi file văn bảnBufferedReader/BufferedWriter
: Đọc ghi file có Buffer
- Hiểu đơn giản hiện tại thì, tất cả dữ liệu sẽ đều là các Input/OutputStream, là cấu phần byte, để đọc ghi dữ liệu
- Thì ở tầng đầu tiên, các class như FileInputStream, FileOutputStream sẽ chỉ có các phương thức
write
cácbytes
. - Các tầng trên thực chất là wrap lại các cách viết, đọc byte này thành các phương thức dạng
readInt
,writeInt
từ các class DataInputStream, DataOutputStream, …
1.2 Về Character Stream và Byte Stream
- ByteStream của Java có thể thực hiện các thao tác vào ra mỗi
8-bit bytes (2^8 - 256) - ASCII
- Còn luồng Character Stream, hay sử dụng các
FileReader FileWriter
thì đọc ghi16-bit unicode
, hỗ trợ được các ngôn ngữ đặc thù hơn (có dấu, gần thì tiếng Việt, các ngôn ngữ khác trên thế giới) do 16-bit unicode cho phép biểu diễn nhiều từ ngữ hơn - Thực chất thì cả 2 luồng này đều là wrap trên FileInputStream/FileOutputStream (cấu phần đọc byte thấp nhất ta nói ở trên), tuy nhiên FileReader/FileWriter sẽ đọc 2 bytes một lần
1.4 BufferReader/Writer
- Với BufferReader thì thực tế ta sẽ cung cấp một bộ đệm ở giữa ở Ram, giúp truy cập nhanh hơn.
Hiểu đơn giản kiểu: Ví dụ trên mạng có một bài báo, bạn mỗi khi muốn đọc thì cần lên mạng, tải trang đó rồi đọc. Nhưng nếu lưu nó về máy thì khi truy cập sẽ nhanh hơn
Câu hỏi: Buffer tốt thế sao không lúc nào cũng dùng ?
- Tuỳ vào bài toán, thông thường thì BufferReader tốt cho mọi trường hợp vì việc sử dụng các bộ nhớ truy cập nhanh (cache, ram) để lưu tạm thời một vùng nhỏ bộ nhớ (rồi từ từ đẩy vào thêm, rồi lại đẩy ra) để đọc ghi là một practice chuẩn, thường được sử dụng trong nhiều lĩnh vực (buffer trong , database cache, front end cache, server cache, quy hoạch động chuẩn bị trước data …)
- tuy nhiên việc vùng nhỏ đó là bao nhiêu cho chuẩn, lúc nào đẩy ra đẩy vào thì là 1 câu chuyện khác. -> Invalidation Cache, các chiến lược để nạp page trong hệ điều hành, …
1.5 Đường dẫn tương đối và tuyệt đối
1.5.1 Đường dẫn (Path) là gì?
Hầu hết các hệ thống tập tin được sử dụng ngày hôm nay lưu trữ các tập tin trong một cây (hoặc cấu trúc phân cấp). Ở đầu cây là một (hoặc nhiều hơn) các nút gốc. Dưới nút gốc, có các tệp và thư mục (thư mục trong Microsoft Windows). Mỗi thư mục có thể chứa các tệp tin và các thư mục con, do đó có thể chứa các tệp và thư mục con, v.v … có khả năng lưu trữ một chiều sâu gần như vô hạn.
1.5.2 Đường dẫn tương đối/tuyệt đối
Một đường dẫn tuyệt đối luôn chứa các phần tử gốc và danh sách thư mục đầy đủ cần thiết để định vị tệp tin. Ví dụ, D:/file.txt
là một đường dẫn tuyệt đối. Tất cả thông tin cần thiết để định vị tệp tin được chứa trong chuỗi đường dẫn.
Một đường dẫn tương đối cần phải được kết hợp với một đường dẫn khác để truy cập một tập tin. Ví dụ là đây là đường dẫn tới file xuất phát từ file dự án
- Ví dụ như ảnh trên, ta có đường dẫn tuyệt đối được trích từ ổ D, rồi vào thư mục Eclipse, …
- Đường dẫn tương đối thì chỉ là từ thư mục dự án equinox/…/file
1.5.3 Ý nghĩa
- Đường dẫn tuyệt đối thì ví dụ khi mang file sang nơi khác để chạy thì có thể sẽ không chạy được, vì có thể do máy người khác dùng một cách sắp thư mục khác (window vs linux), hoặc code thì dùng ổ
D:/
mà máy người dùng không có ổ này, … - Đường dẫn tương đối sẽ được ưa dùng hơn, tức là ta sẽ lưu file nằm gọn trong cùng thư mục dự án, và khi đóng gói thì các thao tác chỉ nằm trong khu vực của dự án
2. Unit Test
2.1. Giới thiệu
Unit Test là một loại kiểm thử phần mềm trong đó các đơn vị hay thành phần riêng lẻ của phần mềm được kiểm thử.
Một Unit là một thành phần PM nhỏ nhất mà ta có thể kiểm tra được như các hàm (Function), thủ tục (Procedure), lớp (Class), hoặc các phương thức (Method).
Ở bài này cơ bản thì, ta sẽ viết các test cho các hàm/lớp trong Java, bằng cách kiểm tra với các input đầu vào định sẵn thì output có ra đúng với ta viết test hay không
- Ví dụ, ta có phương thức tính tổng 2 số, thì ta dự đoán là khi truyền vào 10,20 thì kết quả phải là 30. Input là 10,20 – Output là 30, và ta kiểm tra 30 với kết quả thực tế khi chạy hàm
- Việc kiểm tra sẽ được xử lý qua các test case, ta sẽ viết các test trước. Khi muốn kiểm tra tính năng, ta không phải ngồi thử bằng tay mà có thể chỉ cần ấn chạy test tự động
2.2 Cài đặt các thư viện để test trong Java
- Sẽ cài đặt 2 thư viện hỗ trợ trong việc kiểm thử unit test
- JUnit (https://mvnrepository.com/artifact/junit/junit)
- JUnit Jupiter API (https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api)
- Tuỳ vào dự án đang sử dụng Maven/Gradle thì ta sẽ khai báo thư viện tương ứng để sử dụng
2.3 Assertions, viết Unit Test
Assertions là một thư viện cung cấp một số api phương thức hỗ trợ việc kiểm thử
Ví dụ ảnh trên, ta có 1 hàm tính tổng. Ta viết ra 1 test để thử các input đầu vào để kiểm tra xem expectedSum là kết quả đúng với actualSum là kết quả trả ra của code có đúng không
-> Tưởng tượng như các test input output của codeforce để kiểm tra tính đúng đắn của code
Chúng ta cùng bắt đầu viết thử 1 Unit Test cho đoạn code sau đây bằng thư viện JUnit:
Ví dụ ta có lớp Circle với phương thức sau:
public class Circle {
public static double calculateArea(double radius) {
return Math.PI * radius * radius;
}
}
Sau đó, ta tạo lớp CalculatorTest
trong đường dẫn src/main/test. Trong đó ta sử dụng annotation @Test để định nghĩa cho 1 test.
public class CircleTest {
@Test
public void testCalculateArea() {
//...
}
}
2.4 Một số quy tắc viết Test
Đối với JUnit, ta có một số quy tắc đặt tên sao cho chuẩn như sau:
- Ta phải có từ “Test” ở cuối tên class, ví dụ: LoginControllerTest, StudentControllerTest,… -> Nhận biết file nào là test (Maven sẽ tự động thêm các lớp này vào Test scope)
- Đặt tên các test rõ ràng, ta có thể theo cấu trúc sau: Given[ExplainYourInput]When[WhatIsDone]Then[ExpectedResult] ví dụ:
@Test
public void GivenNullUsernameWhenCreateStudentThenShouldThrowException()
public void Test_01_Khi_Nhap_Vao_So_Am_Thi_Thong_Bao_Sai()
public void Test_02_Khi_Nhap_Vao_So_Duong_Thi_Thong_Bao_Dung()
- Tức là khi cho gì đó (Given) vào thời điểm nào đó (Create) thì nên làm gì (Then)
- Ví dụ: Test cho 1 hàm nhập đầu vào gì đó, nếu người dùng nhập số âm thì chương trình nên trả ra là hãy nhập lại. File test sẽ kiểm tra có thật là chương trình bắt nhập lại hay không ->
Yêu cầu làm thử bài toán trên
- Ngoài ra, các test cần có tính độc lập. Tức là các test có thể thực hiện theo thứ tự ngẫu nhiên, và một test không nên phụ thuộc vào một test khác.
- Ví dụ không nên có chuyện cần chạy test 1 rồi test 6 chạy mới pass
2.5 Chạy thử hàng loạt test và Test Coverage
- Chuột phải vào project, ví dụ như ảnh trên ta có các lựa chọn Run All Test là chạy toàn bộ test trong thư mục test của dự án
2.5.1 Test Coverage là gì
Test coverage (độ bao phủ kiểm thử)
là một phép đo để đánh giá mức độ mà mã nguồn của chương trình đã được kiểm tra thông qua việc chạy các ca kiểm thử. Nó đo lườngtỷ lệ phần trăm của mã nguồn được thực thi bởi các ca kiểm thử
.- Ví dụ, test của ta đã đi hết 90/100 dòng của code thực tế. Tức là đã có 90 dòng đã được chạy trong quá trình test, và kết quả có vẻ đang đúng với
expected
trong test, ta gọi đó làLine Coverage
(bao phủ theo số lượng dòng) - Ngoài ra còn có bao phủ theo số lượng class được chạy, số lượng hàm được chạy
=> Test Coverage
là một chỉ số thể hiện tương đối dự án có đang ở trạng thái được test kiểm tra kĩ hay không. (nếu mà test viết chuẩn) Ví dụ, với những dự án được test coverage rất cao, khoảng 80-90%
, thì dù sau này ta có sửa code, refactor, clean code hay gì đi nữa, ta có thể đảm bảo là khi chạy lại các test ta đã viết, ta sẽ biết chỗ nào có thể đang bị hỏng, chưa đúng để sửa lại
- Mục tiêu của việc đạt test coverage là tăng độ tin cậy của chương trình bằng cách đảm bảo rằng các phần quan trọng của mã nguồn đã được kiểm tra.
Tuy nhiên, test coverage không đảm bảo rằng tất cả các lỗi đều được phát hiện
, và nó cũng không thể đo lường chất lượng của các ca kiểm thử.
- Như ảnh trên, ở More Run/Debug ta có thể có lựa chọn run thử toàn bộ test để đo coverage hiện tại của dự án
- Ta có hình ảnh Inteliji thể hiện bao nhiêu class đã được chạy qua, bao nhiêu method, bao nhiêu line đã được cover.
Tuỳ vào cách đánh giá của công ty, hệ số, ... thì ta sẽ gộp thành 1 coverage tổng thể
- Đây được gọi là kim tự tháp test (testing pyramid), ở bên dưới là các test sẽ nhỏ, độc lập hơn, chạy nhanh hơn. Thường các test này khó móc nối với nhau (kiểu khó thử được 1 luồng là ấn nút giao diện rồi kiểm tra), nên để test ở đây, ta mặc định là mọi thứ đã móc nối chuẩn, rồi viết test sao cho từng module, cấu phần đều chuẩn
- Ở các level cao hơn, test sẽ được móc nối giữa nhiều cấu phần (database, giao diện, nhiều service ngoài, …) sẽ yêu cầu thời gian chạy lâu hơn