Kiểm thử đơn vị tham số hóa cho chương trình Java

MỤC LỤC

Kiểm thử đơn vị

Khi người lập trình thay đổi mã cài đặt của chương trình được kiểm thử thì nếu như các phương thức kiểm thử không được cập nhật theo thì sẽ dẫn đến việc nhiều đường đi thực thi của chương trình sẽ không được kiểm thử. Tuy nhiên, với những chương trình có nhiều đường đi thực thi khác nhau thì việc viết các UT như thế đòi hỏi nhiều thời gian và công sức để tính các giá trị đầu vào thích hợp và khó có thể kiểm tra hết được sự thực thi của chương trình theo tất cả các đường đi.

Kiểm thử đơn vị tham số hóa

    Các ArrayList làm đầu vào kiểm thử có thể là ArrayList không chứa phần tử nào hoặc ArrayList đã chứa một số phần tử, hoặc ArrayList đã chứa đủ số phần tử so với kích cỡ được cấp phát thì khi chèn một đối tượng object có giá trị là null hay các giá trị khác thì đối tượng được chèn vào trở thành phần tử cuối cùng của. Pex sử dụng khả năng của bộ xử lý ràng buộc Z3[31] kết hợp với các lý thuyết toán học khác như hàm chưa định nghĩa, lý thuyết mảng, bit-vetor[2] để giải quyết ràng buộc sinh ra trong quá trình thực thi tượng trưng động và sinh ra các đầu vào kiểm thử cụ thể cho PUT.

    Hình 1: Mối quan hệ giữa UT và PUT
    Hình 1: Mối quan hệ giữa UT và PUT

    Sinh dữ liệu kiểm thử tự động cho PUT

    Thực thi tượng trưng

      Các toán hạng đó có thể là biến, tham số đầu vào hoặc hằng thì giá trị tượng trưng của biến x sẽ là một biểu thức tượng trưng dạng (b) nếu mỗi toán hạng trong biểu thức có một giá trị tượng trưng kết hợp với nó, hoặc là một biểu thức tượng trưng dạng (c) nếu có toán hạng là hằng số hoặc không có giá trị tượng trưng kết hợp với nó. Các ràng buộc này được biểu thị bởi biểu thức của các giá trị tượng trưng hay biểu thức của giá trị tượng trưng và giá trị cụ thể phụ thuộc vào biến xuất hiện trong biểu thức điều kiện của câu lệnh rẽ nhánh có giá trị tượng trưng được tính toán để kết hợp với nó hay không. Nếu biểu thức điều kiện rẽ nhánh nhận giá trị là True thì biểu thức của điều kiện rẽ nhánh sẽ được thu gom vào ràng buộc của PC và được ghi nhớ, đồng thời phủ định của điều kiện rẽ nhánh sẽ được sinh ra và được thêm vào một PC tương ứng với nhánh còn lại mà sự thực thi cụ thể đó không đi theo.

      Ràng buộc S(x)=sym+5 <= 0 lưu vào đỉnh tương ứng của SET cho nhánh mà sự thực thi cụ thể đi theo và được đánh dấu là đã được thăm, đồng thời một đỉnh mới đại diện cho nhánh còn lại được tạo ra và ràng buộc sym+5 > 0 được lưu vào đỉnh này, đỉnh mới được tạo ra được đánh dấu là chưa được thăm.

      Hình 2 : Cây thực thi tượng trưng
      Hình 2 : Cây thực thi tượng trưng

      Xây dựng ràng buộc

        Với các hệ thống kiểm thử mà cài đặt kỹ thuật thực thi tượng trưng ở trên thì cần có các cấu trúc dữ liệu để lưu trữ các giá trị tượng trưng kết hợp với các biến và tham số đầu vào trong quá trình thực thi tượng trưng một chương trình. Sự khác nhau với đầu vào có kiểu không tham chiếu đó là với đầu vào kiểu không tham chiếu ta có thể dễ dàng gán một giá trị cụ thể tới một biến nhưng với đầu vào kiểu đối tượng thì không có giá trị cụ thể như thế được đưa ra để gán cho biến. Trong công cụ khác như JCUTE[9] tất cả các đối tượng làm đầu vào được khởi tạo như một tham chiếu null lần đầu tiên chúng gặp phải trong mỗi lần thực thi và chúng được khởi tạo với các giá trị ngẫu nhiên nếu một ràng buộc yêu cầu chúng là khác null.

        Tuy nhiên, với các lời gọi phương thức thì mã nguồn của phương thức được gọi cần được sửa đổi để cho phép thực thi tương trưng thì phương thức đó mới có thể thực thi tượng trưng với các giá trị tượng trưng được kết hợp với các đối số truyền vào phương thức.

        Bảng 2. Minh họa việc chuyển đổi từ mã nguồn Java sang mã Jimple
        Bảng 2. Minh họa việc chuyển đổi từ mã nguồn Java sang mã Jimple

        Sinh dữ liệu kiểm thử cho PUT

        Vấn đề với các lời gọi phương thức đó là các phương thức được gọi thường không cho phép thực thi tượng trưng như việc gọi các thư viện trong Java, các phương thức mà mã nguồn của nó không sẵn có để ta có thể thêm vào các phần mã cho phép thực thi tượng trưng phương thức đó. Các cạnh của cây được gán nhãn bởi các ràng buộc và các đỉnh (node) của cây đại diện cho sự thực thi của một câu lệnh trong chương trình sao cho đường đi từ đỉnh gốc (root) của cây tới đỉnh lá (leaf) tương ứng với một đường đi thực thi cục bộ (intraprocedural path). Với kỹ thuật thực thi tượng trưng động[13, 19], việc thám hiểm có thể đạt tới đích (đỉnh treo) cho trước bằng thám hiểm lười (lazy exploration) và cố gắng tránh những đường đi mà không đạt tới đích bằng thám hiểm tin cậy (relevant exploration).

        Trong đó lpcn là biểu thức kết hợp của ràng buộc trên các cạnh của đường đi w từ gốc của cây thực thi cục bộ tới đỉnh n và Dg(a) đại diện cho kết quả (function sumary) của hàm g được gọi bởi ƒ với đầu vào a mà xuất hiện trong lpcn.

        Hình 10: Thuật toán sinh dữ liễu kiểm thử
        Hình 10: Thuật toán sinh dữ liễu kiểm thử

        Sinh ca kiểm thử tham số hóa với JPF

        Kiến trúc của JPF

        Search Object chịu trách nhiệm chọn lựa trạng thái để VM tiếp tục quá trình thực thi bằng việc điều khiển VM để sinh ra trạng thái tiếp theo (foward) hoặc quay lui về trạng thái đã được tạo lúc trước. Nói cách khác, MJI cho phép chuyển hướng việc thực thi một chương trình Java bên trong máy ảo JPF sang máy ảo của môi trường thực thi bên dưới (host JVM) mà JPF đang được chạy trên đó. Bộ sinh lựa chọn (CG): JPF sử dụng CG để đạt tới các trạng thái chương trình mong muốn trong trường hợp có nhiều lựa chọn để từ một trạng thái có thể chuyển sang các trạng thái khác.

        Mỗi trạng thái hiện hành của chương trình được đăng ký bởi một đối tượng ChoiceGenerator chứa các sự lựa chọn và trạng thái hiện hành này có thể chuyển tới trạng thái thực thi tiếp theo bằng việc truy vấn tới các giá trị của đối tượng ChoiceGenerator để lựa chọn dãy chỉ thị bytecode sẽ được thực thi tiếp theo.

        Hình 12: Kiến trúc JPF
        Hình 12: Kiến trúc JPF

        Symbolic JPF

          Trong quá trình thực thi tượng trưng, các thông tin tượng trưng được lưu trữ bởi các thuộc tính trong các toán hạng ngăn xếp (stack operand) của ngăn xếp toán hạng (operand stack) tương ứng với stack frame của phương thức đang được thực thi tượng trưng. Ta xét chỉ thị rẽ nhánh IF_ICMPEQ, chỉ thị rẽ nhánh này thực hiện việc so sánh giá trị của 2 biến kiểu int trong điều kiện rẽ nhánh có bằng nhau hay không để điều hướng việc thực thi đi theo nhánh mà giá trị trả về của điều kiện rẽ nhánh là true hoặc false. Để thực thi tượng trưng một phương thức, ta cần cấu hình để JPF sử dụng lớp thông dịch bytecode theo ngữ nghĩa thực thi tương trưng SymbolicInstructionFactory, chỉ định tên phương thức cần thực thi tượng trưng và các đầu vào nào của phương thức là cụ thể hoặc tượng trưng.

          Tất cả các lớp chỉ thị bytecode tượng trưng (các lớp Instruction) chuyển quyền (delegate) thực thi tới các lớp chỉ thị bytecode cha mà cài đặt việc thông dịch bytecode theo ngữ nghĩa thực thi cụ thể khi không có các thông tin tượng trưng lưu trữ trong thuộc tính kết hợp với dữ liệu.

          Hình 15. Trạng thái chương trình thực thi trong Symbolic JPF
          Hình 15. Trạng thái chương trình thực thi trong Symbolic JPF

          Sinh PUT với Symbolic JPF

          Listener có thể chuyển về thực thi cụ thể như xử lý các ràng buộc trong PC hiện thời, tính toán các giá trị cụ thể tương ứng với các biến và tiếp tục thực thi trong chế độ thực thi cụ thể. Với một phương thức có nhiều đầu vào ta có thể chỉ định một số đầu vào là cụ thể để giảm đi sự phức tạp của các điều kiện đường đi trong việc thực thi tượng trưng phương thức đó. Bằng cách viết các ca kiểm thử tham số hóa dựa trên nền kiểm thử JUnit ta sẽ giảm được công sức để xây dựng lại thư viện xác nhận (assertion) và giúp việc sinh lại các ca kiểm thử đơn vị JUnit dễ dàng hơn.

          Hệ thống của ta sẽ mở rộng dễ dàng để thực thi các ca kiểm thử tham số hóa nhận các tham số đầu vào có kiểu tham chiếu khác nếu như Symbolic JPF được mở rộng để thực thi tượng trưng những phương thức như thế.

          Hình 17: Kiến trúc hệ thống cài đặt
          Hình 17: Kiến trúc hệ thống cài đặt

          Mở rộng Symbolic JPF

            Tuy nhiên, ta sẽ không quay lại để nghiên cứu xem làm sao có thể sử dụng JPF để thực thi các chương trình đã được sửa đổi như thế mà cách tiếp cận của ta ở đây là sẽ sử dụng các thư viện hỗ trợ thực thi tượng trưng và ý tưởng khởi tạo lười để thay đổi nghữ nghĩa thực thi các chỉ thị bytecode theo ngữ nghĩa thực thi tượng trưng sao cho JPF có thể thực thi tượng trưng các chương trình nhận đầu vào có kiểu cấu trúc dữ liệu phức tạp. Nếu như với kiểu số học thì các giá trị tượng trưng của các biến được kết hợp với các thuộc tính (attributes) của toán hạng ngăn xếp (stack operands) của ngăn xếp (operand stack) trong stack frame và ở các vùng biến cục bộ thì đối với kiểu String hay các kiểu tham chiếu đối tượng khác các giá trị tượng trưng sẽ được lưu trữ trong vùng nhớ heap. Do đó để có thể thay đổi ngữ nghĩa thực thi đối với các chỉ thị bytecode mà thực hiện việc so sánh, tính toán với kiểu tham chiếu thì ta cần có cơ chế để lấy cũng như cập nhật các giá trị tượng trưng được kết hợp với các tham chiếu đó trong vùng nhớ heap để xử lý cùng với thao tác tính toán diễn ra trên ngăn xếp toán hạng của stack frame.

            Nếu như với các biến có kiểu dữ liệu nguyên thủy (int, float, double, long) thì giá trị của nó được đưa trực tiếp lên stack để so sánh và các giá trị tượng trưng của nó được kết hợp luôn cùng các thuộc tính của operand stack thì với kiểu dữ liệu String ở trên cũng như các đối tượng khác thì việc so sánh thực hiện với các giá trị số học đại diện cho chúng còn bản thân các đối tượng đó vẫn lưu trong vùng heap.