| name | create-codeql-query-unit-test-csharp |
|---|---|
| description | Create comprehensive unit tests for CodeQL queries targeting C# code. Use this skill when you need to create, validate, or enhance test coverage for C# CodeQL queries using the CodeQL Development MCP Server tools. |
This skill guides you through creating unit tests for CodeQL queries that analyze C# code.
- Creating new unit tests for a C# CodeQL query
- Adding test cases to existing C# query tests
- Validating C# query behavior against known code patterns
- A CodeQL query (
.qlfile) that you want to test - Understanding of what C# code patterns the query should detect
- Access to CodeQL Development MCP Server tools
Organize tests in your query pack's test directory:
<query-pack>/test/{QueryName}/
├── {QueryName}.qlref # Reference to the query being tested
├── Example1.cs # Primary test source file
├── Example2.cs # Additional test cases (optional)
├── {QueryName}.expected # Expected query results
└── {QueryName}.testproj/ # Generated test database (auto-created)
- Test source files: Use
Example1.cs,Example2.cs, etc. ortest.cs - Query reference:
{QueryName}.qlref(exact match to query directory name) - Expected results:
{QueryName}.expected(exact match to query name)
mkdir -p <query-pack>/test/{QueryName}Create {QueryName}.qlref with the relative path to your query:
{QueryName}/{QueryName}.ql
Important: Path is relative to your query pack's source directory (where your queries are organized).
Create Example1.cs with test cases:
Positive Cases (should be detected):
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
// Test case: Unsafe pattern
public class UnsafeExample
{
public void UnsafeOperation(string input)
{
var formatter = new BinaryFormatter();
var stream = new MemoryStream(Convert.FromBase64String(input));
var obj = formatter.Deserialize(stream); // Should detect
}
}Negative Cases (should NOT be detected):
using System;
// Test case: Safe pattern
public class SafeExample
{
public void SafeOperation(string input)
{
if (IsValid(input))
{
// Safe operation
}
}
private bool IsValid(string input) => !string.IsNullOrEmpty(input);
}C#-Specific Considerations:
- Include necessary
usingstatements - Test relevant C# features (properties, LINQ, async/await, pattern matching)
- For security queries, include .NET-specific patterns (SQL injection with
SqlCommand, XSS withHttpUtility) - Test ASP.NET patterns for web queries
Create {QueryName}.expected:
| file | line | col | endLine | endCol | message |
| Example1.cs | 12 | 19 | 12 | 46 | Unsafe deserialization |
- Column/line numbers are 1-indexed
- Match query output exactly
Use codeql_test_extract:
{
"testPath": "<query-pack>/test/{QueryName}",
"searchPath": ["<query-pack>"]
}Creates database at test/{QueryName}/{QueryName}.testproj/.
Run PrintAST to understand C# AST:
{
"query": "<query-pack>/src/PrintAST/PrintAST.ql",
"database": "<query-pack>/test/{QueryName}/{QueryName}.testproj",
"searchPath": ["<query-pack>"]
}Key C# AST nodes: Class, Method, Property, MethodCall, QueryExpr, AwaitExpr
Execute tests with codeql_test_run:
{
"testPath": "<query-pack>/test/{QueryName}",
"searchPath": ["<query-pack>"]
}✅ Pass: Output matches .expected exactly
❌ Fail: Differences require query or expected file updates
- Review actual query output
- Compare with expected results
- Update query logic or
.expectedfile - Re-run tests
- Repeat until passing
Use codeql_test_accept to update baseline (only after verification).
Create additional files (Example2.cs, etc.), update .expected, re-extract database, and re-run tests.
codeql_test_extract: Extract test databases from C# source codecodeql_test_run: Run query tests and compare with expected resultscodeql_test_accept: Accept actual results as new baseline (use with caution)codeql_query_compile: Compile CodeQL queriescodeql_query_run: Run queries (e.g., PrintAST)codeql_bqrs_decode: Decode binary query resultscodeql_pack_install: Install query pack dependencies
❌ Don't:
- Forget
usingstatements - Use incorrect file extensions (
.csnot.cpp) - Skip negative test cases
- Hardcode expected line numbers without verifying
✅ Do:
- Write compilable C# code
- Include positive, negative, and edge cases
- Document each test case with comments
- Verify line/column numbers in
.expectedmatch source
<query-pack>/test/FindUnsafeDeserialization/
├── FindUnsafeDeserialization.qlref
├── Example1.cs
├── FindUnsafeDeserialization.expected
└── FindUnsafeDeserialization.testproj/ (auto-generated)
FindUnsafeDeserialization.qlref:
FindUnsafeDeserialization/FindUnsafeDeserialization.ql
Example1.cs:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;
// Test case 1: Unsafe deserialization (should detect)
public class TestCase1
{
public void UnsafeDeserialize(byte[] data)
{
var formatter = new BinaryFormatter();
var stream = new MemoryStream(data);
var obj = formatter.Deserialize(stream); // Unsafe: untrusted data
}
}
// Test case 2: Safe with validation (should NOT detect)
public class TestCase2
{
public void SafeDeserialize(byte[] data)
{
if (ValidateSignature(data))
{
var formatter = new BinaryFormatter();
var stream = new MemoryStream(data);
var obj = formatter.Deserialize(stream); // Safe: validated
}
}
private bool ValidateSignature(byte[] data) => true;
}
// Test case 3: Async context (should detect)
public class TestCase3
{
public async Task<object> DeserializeAsync(byte[] data)
{
await Task.Delay(10);
var formatter = new BinaryFormatter();
var stream = new MemoryStream(data);
return formatter.Deserialize(stream); // Unsafe: async
}
}FindUnsafeDeserialization.expected:
| file | line | col | endLine | endCol | message |
| Example1.cs | 13 | 19 | 13 | 46 | Unsafe deserialization |
| Example1.cs | 40 | 16 | 40 | 43 | Unsafe deserialization |
- ✅ Test structure follows conventions
- ✅ C# test code compiles and is valid
- ✅ Test database extracts without errors
- ✅ All tests pass consistently
- ✅ Both positive and negative cases included
- ✅ Expected results accurately reflect query behavior