8i | 9i | 10g | 11g | 12c | 13c | 18c | 19c | 21c | 23ai | 24ai | 26ai | Misc | PL/SQL | SQL | RAC | WebLogic | Linux
Code Coverage Using DBMS_PLSQL_CODE_COVERAGE in Oracle Database 12c Release 2 (12.2)
Code coverage is a measure of how much code was touched by your test suite. The higher percentage of code coverage, the less likely you are to have bugs present in untested code. You can read a description of code coverage here.
The DBMS_PLSQL_CODE_COVERAGE package was introduced in Oracle Database 12c Release 2 (12.2) to provide an API to capture code coverage information during testing.
It is unlikely you will use this package directly. Instead your unit testing tool or framework is likely to make use of it. Code coverage is usually discussed in conjunction with unit testing, but unit testing is out of scope for this article.
- The Issue
- Basic Code Coverage Process
- Code Coverage : Example 1
- Code Coverage : Example 2
- COVERAGE Pragma
- Opinions
The Issue
Imagine the following function is part of our application and we want to build a test harness.
CREATE OR REPLACE FUNCTION func1 (p_code IN VARCHAR2)
RETURN VARCHAR2
AS
BEGIN
-- Validate input.
IF p_code IS NULL THEN
DBMS_OUTPUT.put_line('Parameter P_CODE cannot be NULL.');
RETURN 'Error';
ELSIF LENGTH(p_code) NOT BETWEEN 3 and 5 THEN
DBMS_OUTPUT.put_line('Parameter P_CODE must be between 3-5 characters inclusive.');
RETURN 'Error';
ELSIF TO_NUMBER(p_code DEFAULT -1 ON CONVERSION ERROR) != -1 THEN
DBMS_OUTPUT.put_line('Parameter P_CODE must contain at least 1 non-numeric character.');
RETURN 'Error';
END IF;
-- The parameter is good, so do something.
RETURN LOWER(p_code);
END;
/
We create the following procedure to run tests against the function.
CREATE OR REPLACE PROCEDURE run_func1_test(
p_test IN VARCHAR2,
p_code IN VARCHAR2,
p_return IN VARCHAR2)
AS
l_return VARCHAR2(32767);
BEGIN
DBMS_OUTPUT.put_line('----------------------------------------');
DBMS_OUTPUT.put_line('p_test=' || p_test || ' : p_code=' || p_code || ' : p_return=' || p_return);
l_return := func1(p_code);
DBMS_OUTPUT.put_line('l_return=' || l_return);
DBMS_OUTPUT.put(p_test || ' Result=');
IF l_return = p_return THEN
DBMS_OUTPUT.put_line('Passed');
ELSE
DBMS_OUTPUT.put_line('Failed');
END IF;
END run_func1_test;
/
Our test harness now looks like this.
SET SERVEROUTPUT ON
BEGIN
run_func1_test('Test 1', 'ABC', 'abc');
END;
/
----------------------------------------
p_test=Test 1 : p_code=ABC : p_return=abc
l_return=abc
Test 1 Result=Passed
PL/SQL procedure successfully completed.
SQL>
We are happy our code passed the test, but we are concerned the test harness is not really testing all possible aspects of the code.
Basic Code Coverage Process
The following actions represent the basic process of measuring code coverage.
- Create the code coverage tables.
- Start code coverage.
- Run your tests.
- Stop code coverage.
- Query the code coverage tables.
The process will feel very familiar if you have ever used the DBMS_PROFILER or DBMS_HPROF packages. We can see the pieces that make up the code coverage process below.
As a one-off task, create the tables to hold the code coverage information. You only need to do this once. The FORCE_IT parameter forces the existing tables to be dropped and recreated.
BEGIN
DBMS_PLSQL_CODE_COVERAGE.create_coverage_tables(
force_it => TRUE);
END;
/
Start the process of gathering code coverage information.
SET SERVEROUTPUT ON
DECLARE
l_run NUMBER;
BEGIN
l_run := DBMS_PLSQL_CODE_COVERAGE.start_coverage(run_comment => 'Example Run');
DBMS_OUTPUT.put_line('l_run=' || l_run);
END;
/
Run your PL/SQL test suite now.
Stop code coverage.
BEGIN DBMS_PLSQL_CODE_COVERAGE.stop_coverage; END; /
The code coverage information will be in the following tables.
SQL> DESC dbmspcc_runs Name Null? Type ----------------------------------------- -------- ---------------------------- RUN_ID NOT NULL NUMBER(38) RUN_COMMENT VARCHAR2(4000) RUN_OWNER NOT NULL VARCHAR2(128) RUN_TIMESTAMP NOT NULL DATE SQL> DESC dbmspcc_units Name Null? Type ----------------------------------------- -------- ---------------------------- RUN_ID NOT NULL NUMBER(38) OBJECT_ID NOT NULL NUMBER(38) OWNER NOT NULL VARCHAR2(128) NAME NOT NULL VARCHAR2(128) TYPE NOT NULL VARCHAR2(12) LAST_DDL_TIME NOT NULL DATE SQL> DESC dbmspcc_blocks Name Null? Type ----------------------------------------- -------- ---------------------------- RUN_ID NOT NULL NUMBER(38) OBJECT_ID NOT NULL NUMBER(38) BLOCK NOT NULL NUMBER(38) LINE NOT NULL NUMBER(38) COL NOT NULL NUMBER(38) COVERED NOT NULL NUMBER(1) NOT_FEASIBLE NOT NULL NUMBER(1) SQL>
Code Coverage : Example 1
We start by checking the code coverage of our existing test harness.
SET SERVEROUTPUT ON
DECLARE
l_run NUMBER;
BEGIN
l_run := DBMS_PLSQL_CODE_COVERAGE.start_coverage(run_comment => 'Example 1');
DBMS_OUTPUT.put_line('l_run=' || l_run);
-- Run our tests.
run_func1_test('Test 1', 'ABC', 'abc');
DBMS_PLSQL_CODE_COVERAGE.stop_coverage;
END;
/
l_run=54
----------------------------------------
p_test=Test 1 : p_code=ABC : p_return=abc
l_return=abc
Test 1 Result=Passed
PL/SQL procedure successfully completed.
SQL>
We already know the RUN_ID is 54 because we displayed it above, but we can see the run has been recorded if we search using the run comment.
COLUMN run_comment FORMAT A30
SELECT run_id,
run_comment
FROM dbmspcc_runs
WHERE run_comment = 'Example 1';
RUN_ID RUN_COMMENT
---------- ------------------------------
54 Example 1
SQL>
We check the objects that were touched by the run.
COLUMN owner FORMAT A20
COLUMN name FORMAT A20
COLUMN type FORMAT A20
SELECT object_id,
owner,
name,
type
FROM dbmspcc_units
WHERE run_id = 54
ORDER BY 1;
OBJECT_ID OWNER NAME TYPE
---------- -------------------- -------------------- --------------------
79206 TEST FUNC1 FUNCTION
79220 TEST RUN_FUNC1_TEST PROCEDURE
SQL>
We only want to check the code coverage of the FUNC1 function, not the RUN_FUNC1_TEST procedure, so we will exclude that from the results of the DBMSPCC_BLOCKS table.
SELECT block,
line,
col,
covered,
not_feasible
FROM dbmspcc_blocks
WHERE run_id = 54
AND object_id = 79206
ORDER BY 2;
BLOCK LINE COL COVERED NOT_FEASIBLE
---------- ---------- ---------- ---------- ------------
1 1 1 1 0
2 7 5 0 0
4 9 9 0 0
3 9 9 1 0
6 9 9 1 0
7 9 9 0 0
5 10 5 0 0
10 12 58 0 0
8 12 9 1 0
11 13 5 0 0
9 18 3 1 0
11 rows selected.
SQL>
That's not too useful on its own, so lets combine this with the source code.
SET LINESIZE 1000 PAGESIZE 100
COLUMN col FORMAT A10
COLUMN covered FORMAT A10
COLUMN line FORMAT 99999
COLUMN text FORMAT A110
SELECT LISTAGG(ccb.col, ',') WITHIN GROUP (ORDER BY ccb.col) AS col,
LISTAGG(ccb.covered, ',') WITHIN GROUP (ORDER BY ccb.col) AS covered,
s.line,
s.text
FROM user_source s
JOIN dbmspcc_units ccu ON s.name = ccu.name AND s.type = ccu.type
LEFT OUTER JOIN dbmspcc_blocks ccb ON ccu.run_id = ccb.run_id AND ccu.object_id = ccb.object_id AND s.line = ccb.line
WHERE s.name = 'FUNC1'
AND s.type = 'FUNCTION'
AND ccu.run_id = 54
GROUP BY s.line, s.text
ORDER BY 3;
COL COVERED LINE TEXT
---------- ---------- ------ --------------------------------------------------------------------------------------------------------------
1 1 1 FUNCTION func1 (p_code IN VARCHAR2)
2 RETURN VARCHAR2
3 AS
4 BEGIN
5 -- Validate input.
6 IF p_code IS NULL THEN
5 0 7 DBMS_OUTPUT.put_line('Parameter P_CODE cannot be NULL.');
8 RETURN 'Error';
9,9,9,9 0,0,1,1 9 ELSIF LENGTH(p_code) NOT BETWEEN 3 and 5 THEN
5 0 10 DBMS_OUTPUT.put_line('Parameter P_CODE must be between 3-5 characters inclusive.');
11 RETURN 'Error';
9,58 1,0 12 ELSIF TO_NUMBER(p_code DEFAULT -1 ON CONVERSION ERROR) != -1 THEN
5 0 13 DBMS_OUTPUT.put_line('Parameter P_CODE must contain at least 1 non-numeric character.');
14 RETURN 'Error';
15 END IF;
16
17 -- The parameter is good, so do something.
3 1 18 RETURN LOWER(p_code);
19 END;
19 rows selected.
SQL>
The output is a little clumsy, but we can see that lines 7, 10 and 13 weren't covered/touched by our test, so we really need to add some extra tests to cover them.
Code Coverage : Example 2
We add some extra tests to the test harness and check the code coverage. Notice we got the expected results for each test, including those we expected to cause an error.
SET SERVEROUTPUT ON
DECLARE
l_run NUMBER;
BEGIN
l_run := DBMS_PLSQL_CODE_COVERAGE.start_coverage(run_comment => 'Example 2');
DBMS_OUTPUT.put_line('l_run=' || l_run);
-- Run our tests.
run_func1_test('Test 1', 'ABC', 'abc');
run_func1_test('Test 2', NULL, 'Error');
run_func1_test('Test 3', 'AB', 'Error');
run_func1_test('Test 4', 'ABCDEF', 'Error');
run_func1_test('Test 5', '111', 'Error');
DBMS_PLSQL_CODE_COVERAGE.stop_coverage;
END;
/
l_run=55
----------------------------------------
p_test=Test 1 : p_code=ABC : p_return=abc
l_return=abc
Test 1 Result=Passed
----------------------------------------
p_test=Test 2 : p_code= : p_return=Error
Parameter P_CODE cannot be NULL.
l_return=Error
Test 2 Result=Passed
----------------------------------------
p_test=Test 3 : p_code=AB : p_return=Error
Parameter P_CODE must be between 3-5 characters inclusive.
l_return=Error
Test 3 Result=Passed
----------------------------------------
p_test=Test 4 : p_code=ABCDEF : p_return=Error
Parameter P_CODE must be between 3-5 characters inclusive.
l_return=Error
Test 4 Result=Passed
----------------------------------------
p_test=Test 5 : p_code=111 : p_return=Error
Parameter P_CODE must contain at least 1 non-numeric character.
l_return=Error
Test 5 Result=Passed
PL/SQL procedure successfully completed.
SQL>
Checking the code coverage information against the source code we can see our tests now covered rows 7, 10 and 13 in our code, so we have tested more of the code base.
SET LINESIZE 1000 PAGESIZE 100
COLUMN line FORMAT 99999
COLUMN text FORMAT A110
COLUMN col FORMAT A10
COLUMN covered FORMAT A10
SELECT LISTAGG(ccb.col, ',') WITHIN GROUP (ORDER BY ccb.col) AS col,
LISTAGG(ccb.covered, ',') WITHIN GROUP (ORDER BY ccb.col) AS covered,
s.line,
s.text
FROM user_source s
JOIN dbmspcc_units ccu ON s.name = ccu.name AND s.type = ccu.type
LEFT OUTER JOIN dbmspcc_blocks ccb ON ccu.run_id = ccb.run_id AND ccu.object_id = ccb.object_id AND s.line = ccb.line
WHERE s.name = 'FUNC1'
AND s.type = 'FUNCTION'
AND ccu.run_id = 55
GROUP BY s.line, s.text
ORDER BY 3;
COL COVERED LINE TEXT
---------- ---------- ------ --------------------------------------------------------------------------------------------------------------
1 1 1 FUNCTION func1 (p_code IN VARCHAR2)
2 RETURN VARCHAR2
3 AS
4 BEGIN
5 -- Validate input.
6 IF p_code IS NULL THEN
5 1 7 DBMS_OUTPUT.put_line('Parameter P_CODE cannot be NULL.');
8 RETURN 'Error';
9,9,9,9 0,0,1,1 9 ELSIF LENGTH(p_code) NOT BETWEEN 3 and 5 THEN
5 1 10 DBMS_OUTPUT.put_line('Parameter P_CODE must be between 3-5 characters inclusive.');
11 RETURN 'Error';
9,58 1,1 12 ELSIF TO_NUMBER(p_code DEFAULT -1 ON CONVERSION ERROR) != -1 THEN
5 1 13 DBMS_OUTPUT.put_line('Parameter P_CODE must contain at least 1 non-numeric character.');
14 RETURN 'Error';
15 END IF;
16
17 -- The parameter is good, so do something.
3 1 18 RETURN LOWER(p_code);
19 END;
19 rows selected.
SQL>
COVERAGE Pragma
If you have a section of code you don't want to include in your coverage tests you can mark it as not feasible. The rows associated with these markers can then be excluded from any reporting of code coverage.
Individual blocks of code can be marked as not feasible using PRAGMA COVERAGE with the 'NOT_FEASIBLE' argument.
CREATE OR REPLACE FUNCTION func1 (p_code IN VARCHAR2)
RETURN VARCHAR2
AS
BEGIN
-- Validate input.
IF p_code IS NULL THEN
DBMS_OUTPUT.put_line('Parameter P_CODE cannot be NULL.');
RETURN 'Error';
ELSIF LENGTH(p_code) NOT BETWEEN 3 and 5 THEN
PRAGMA COVERAGE ('NOT_FEASIBLE');
DBMS_OUTPUT.put_line('Parameter P_CODE must be between 3-5 characters inclusive.');
RETURN 'Error';
ELSIF TO_NUMBER(p_code DEFAULT -1 ON CONVERSION ERROR) != -1 THEN
PRAGMA COVERAGE ('NOT_FEASIBLE');
DBMS_OUTPUT.put_line('Parameter P_CODE must contain at least 1 non-numeric character.');
RETURN 'Error';
END IF;
-- The parameter is good, so do something.
RETURN LOWER(p_code);
END;
/
-- Recompile test harness, or you get erratic results.
ALTER PROCEDURE run_func1_test COMPILE;
SET SERVEROUTPUT ON
DECLARE
l_run NUMBER;
BEGIN
l_run := DBMS_PLSQL_CODE_COVERAGE.start_coverage(run_comment => 'COVERAGE Pragma');
DBMS_OUTPUT.put_line('l_run=' || l_run);
-- Run our tests.
run_func1_test('Test 1', 'ABC', 'abc');
DBMS_PLSQL_CODE_COVERAGE.stop_coverage;
END;
/
l_run=56
----------------------------------------
p_test=Test 1 : p_code=ABC : p_return=abc
l_return=abc
Test 1 Result=Passed
PL/SQL procedure successfully completed.
SQL>
CLEAR COLUMN
SELECT block,
line,
col,
covered,
not_feasible
FROM dbmspcc_blocks
WHERE run_id = 56
AND object_id = 79206
ORDER BY 2;
BLOCK LINE COL COVERED NOT_FEASIBLE
---------- ---------- ---------- ---------- ------------
1 1 1 1 0
2 7 5 0 0
4 9 9 0 0
3 9 9 1 0
6 9 9 1 0
7 9 9 0 0
5 10 5 0 1
10 13 58 0 0
8 13 9 1 0
11 14 5 0 1
9 20 3 1 0
11 rows selected.
SQL>
Using the 'NOT_FEASIBLE_START' and 'NOT_FEASIBLE_END' arguments allows you to mark a range of lines as not feasible.
CREATE OR REPLACE FUNCTION func1 (p_code IN VARCHAR2)
RETURN VARCHAR2
AS
BEGIN
PRAGMA COVERAGE ('NOT_FEASIBLE_START');
DBMS_OUTPUT.put_line('This line should be not feasible.');
PRAGMA COVERAGE ('NOT_FEASIBLE_END');
-- Validate input.
IF p_code IS NULL THEN
DBMS_OUTPUT.put_line('Parameter P_CODE cannot be NULL.');
RETURN 'Error';
ELSIF LENGTH(p_code) NOT BETWEEN 3 and 5 THEN
DBMS_OUTPUT.put_line('Parameter P_CODE must be between 3-5 characters inclusive.');
RETURN 'Error';
ELSIF TO_NUMBER(p_code DEFAULT -1 ON CONVERSION ERROR) != -1 THEN
DBMS_OUTPUT.put_line('Parameter P_CODE must contain at least 1 non-numeric character.');
RETURN 'Error';
END IF;
-- The parameter is good, so do something.
RETURN LOWER(p_code);
END;
/
-- Recompile test harness, or you get erratic results.
ALTER PROCEDURE run_func1_test COMPILE;
SET SERVEROUTPUT ON
DECLARE
l_run NUMBER;
BEGIN
l_run := DBMS_PLSQL_CODE_COVERAGE.start_coverage(run_comment => 'COVERAGE Pragma');
DBMS_OUTPUT.put_line('l_run=' || l_run);
-- Run our tests.
run_func1_test('Test 1', 'ABC', 'abc');
DBMS_PLSQL_CODE_COVERAGE.stop_coverage;
END;
/
l_run=57
----------------------------------------
p_test=Test 1 : p_code=ABC : p_return=abc
This line should be not feasible.
l_return=abc
Test 1 Result=Passed
PL/SQL procedure successfully completed.
SQL>
SELECT block,
line,
col,
covered,
not_feasible
FROM dbmspcc_blocks
WHERE run_id = 57
AND object_id = 79206
ORDER BY 2;
BLOCK LINE COL COVERED NOT_FEASIBLE
---------- ---------- ---------- ---------- ------------
1 1 1 1 1
2 11 5 0 0
4 13 9 0 0
3 13 9 1 0
6 13 9 1 0
7 13 9 0 0
5 14 5 0 0
10 16 58 0 0
8 16 9 1 0
11 17 5 0 0
9 22 3 1 0
11 rows selected.
SQL>
You can also mark a whole stored object as not feasible.
CREATE OR REPLACE FUNCTION func1 (p_code IN VARCHAR2)
RETURN VARCHAR2
AS
PRAGMA COVERAGE ('NOT_FEASIBLE_START');
BEGIN
-- Validate input.
IF p_code IS NULL THEN
DBMS_OUTPUT.put_line('Parameter P_CODE cannot be NULL.');
RETURN 'Error';
ELSIF LENGTH(p_code) NOT BETWEEN 3 and 5 THEN
DBMS_OUTPUT.put_line('Parameter P_CODE must be between 3-5 characters inclusive.');
RETURN 'Error';
ELSIF TO_NUMBER(p_code DEFAULT -1 ON CONVERSION ERROR) != -1 THEN
DBMS_OUTPUT.put_line('Parameter P_CODE must contain at least 1 non-numeric character.');
RETURN 'Error';
END IF;
-- The parameter is good, so do something.
PRAGMA COVERAGE ('NOT_FEASIBLE_END');
RETURN LOWER(p_code);
END;
/
-- Recompile test harness, or you get erratic results.
ALTER PROCEDURE run_func1_test COMPILE;
SET SERVEROUTPUT ON
DECLARE
l_run NUMBER;
BEGIN
l_run := DBMS_PLSQL_CODE_COVERAGE.start_coverage(run_comment => 'COVERAGE Pragma');
DBMS_OUTPUT.put_line('l_run=' || l_run);
-- Run our tests.
run_func1_test('Test 1', 'ABC', 'abc');
DBMS_PLSQL_CODE_COVERAGE.stop_coverage;
END;
/
l_run=58
----------------------------------------
p_test=Test 1 : p_code=ABC : p_return=abc
l_return=abc
Test 1 Result=Passed
PL/SQL procedure successfully completed.
SQL>
SELECT block,
line,
col,
covered,
not_feasible
FROM dbmspcc_blocks
WHERE run_id = 58
AND object_id = 79206
ORDER BY 2;
BLOCK LINE COL COVERED NOT_FEASIBLE
---------- ---------- ---------- ---------- ------------
1 1 1 1 1
2 9 5 0 1
4 11 9 0 1
3 11 9 1 1
6 11 9 1 1
7 11 9 0 1
5 12 5 0 1
10 14 58 0 1
8 14 9 1 1
11 15 5 0 1
9 20 3 1 1
11 rows selected.
SQL>
Opinions
Here are some opinions on this feature.
- This feels like a half-finished feature. I would have expected some sort of reporting out of the box, like a pipelined table function that produces a report like DBMS_XPLAN, or a HTML report like DBMS_HPROF. Without a prebuilt report it can be awkward to interpret the data at times. I imagine it would be almost unusable for a large piece of code without a tool fronting it.
- The feature feels a little buggy. I could recompile my code, run my test harness, get correct results out of my test harness, but get incorrect results out of the code coverage. If I recompiled my test harness each time, I could then get consistent results. This sounds trivial, but in a real system having to keep an eye on the dependencies manually like this seems unworkable.
- This feature feels a little out of place without a full unit testing framework, but as I said earlier it will be used by unit testing frameworks. SQL Developer already includes unit testing and code coverage, which uses the
DBMS_PLSQL_CODE_COVERAGEpackage under the hood. I would probably suggest using SQL Developer or utPLSQL rather than using this functionality directly.
For more information see:
- Code Coverage (Wikipedia)
- Using PL/SQL Basic Block Coverage to Maintain Quality
- DBMS_PLSQL_CODE_COVERAGE
- COVERAGE Pragma
Hope this helps. Regards Tim...