jQAssistant Demo Guide
Introduction
jQAssistant is a QA tool that allows the definition and validation of project-specific rules on a structural level. It is built upon the Neo4j graph database and can easily be plugged into the build process to automate detection of constraint violations and generate reports about user-defined concepts and metrics.
Example use cases:
-
Enforce naming conventions (e.g. EJBs, JPA entities, test classes, packages, maven modules, etc.)
-
Validate dependencies between modules of your project
-
Separate API and implementation packages
-
Detect common problems like cyclic dependencies or tests without assertions
Using jQAssistant
The rules are expressed in Cypher - Neo4j’s easy-to-learn query language:
MATCH (t:Test:Method)
WHERE NOT (t)-[:INVOKES]->(:Assert:Method)
RETURN t AS TestWithoutAssertion
License: jQAssistant is contributed under GNU General Public License, v3.
Scan Process
If you haven’t scanned your project yet, please download jQAssistant here.
Then run the scan of your project with this command:
bin/jqassistant.sh scan -f lib
It will scan the following aspects:
-
Java class and jar files
-
Maven pom.xml
-
XML configuration files
-
Spring Configuration
-
Persistence.xml
-
…and more
Start Neo4j and run jqa
Then you can start your Neo4j server with
bin/jqassistant.sh server
and run this guide with :play https://guides.neo4j.com/jqa
inside that server.
Data Overview
If you have your Neo4j instance running on the scanned data, you can get a quick overview on the data model with this query
call db.schema()
For a tabular overview use this:
MATCH (n)
// a collection of multiple labels is turned into rows
UNWIND labels(n) as label
RETURN label, count(*)
ORDER BY count(*) asc
Analysis: Which class extends FROM another class?
This query shows the first 20 FQNs of classes and their super-type (class or interface).
MATCH (c1:Class)-[:EXTENDS]->(c2:Type)
RETURN c1.fqn, c2.fqn
LIMIT 20
Analysis: Which classes contain the highest number of methods?
Each Type DECLARES
members, i.e. links to Method
or Field
nodes.
Here we just count the methods and return the top 20 offenders.
MATCH (class:Class)-[:DECLARES]->(method:Method)
RETURN class.fqn, count(method) AS Methods
ORDER BY Methods DESC
LIMIT 20
Analysis: Which class has the deepest inheritance hierarchy?
We follow the EXTENDS relationship transitively up to the top of the hierarchy and return the top 20 longest inheritance chains.
MATCH h = (class:Class)-[:EXTENDS*]->(super:Type)
WHERE NOT EXISTS((super)-[:EXTENDS]->())
RETURN class.fqn, length(h) AS Depth
ORDER BY Depth DESC
LIMIT 20
Analysis: Which classes are affected by certain exceptions?
Now we want to know which methods are transitively calling a constructor of the given exception type.
MATCH (e:Type)-[:DECLARES]->(init:Constructor)
WHERE e.fqn = 'java.io.IOException'
WITH e, init
MATCH (type:Type)-[:DECLARES]->(method:Method)
MATCH path = (method)-[:INVOKES*]->(init)
RETURN type, path LIMIT 10
Analysis: How many methods call something in a given package?
It would be interesting to know how many methods are affected if you change the return type of a method. Or how much effort it would take to decouple some architectural artifacts.
MATCH (caller:Method:Java)-[:INVOKES]->(callee:Method:Java)<-[:DECLARES]-(t:Type)
WHERE t.fqn STARTS WITH {package}
RETURN t.fqn, callee.name, count(caller) AS callers
ORDER BY callers
Visibility: Find unnecessary public visibility
First step: put a label ‘Public’ on the public methods.
MATCH (m:Method)
WHERE m.visibility='public'
SET m:Public
Visibility: step 2
Second step - Report top 20 public methods which are called from within the same package.
MATCH (package:Package)-[:CONTAINS]->(t1:Type)-[:DECLARES]->(m:Method),
(package:Package)-[:CONTAINS]->(t2:Type)-[:DECLARES]->(p:Method:Public),
(m)-[:INVOKES]->(p)
WHERE t1 <> t2
WITH p, t2, count(*) as freq
ORDER BY freq DESC LIMIT 20
RETURN p.name, t2.fqn, freq
Immutability: Label classes with an immutable state as "Immutable"
MATCH (immutable:Class)-[:DECLARES]->(field:Field)<-[:WRITES]-(accessorMethod)
WHERE field.visibility = 'private'
WITH immutable, collect(accessorMethod) AS accessorMethods
WHERE ALL(accessorMethod IN accessorMethods WHERE accessorMethod:Constructor)
SET immutable:Immutable
RETURN immutable
Further analysis: Mark types to investigate
Mark the types in one package to be investigated.
So instead of always checking this condition: WHERE exists(t.byteCodeVersion) AND t.fqn STARTS WITH 'some.package.'
, we can just match on the :Investigate
label.
MATCH (t:Type:File)<-[:DEPENDS_ON]-(dependent:Type)
WHERE exists(t.byteCodeVersion) AND t.fqn STARTS WITH {package}
SET t:Investigate
Further analysis: Add fan-in to type
Let’s add a property 'fanIn' to a Type with the number of other types depending on it.
MATCH (t:Type:File:Investigate)<-[:DEPENDS_ON]-(dependent:Type)
WITH t, count(dependent) AS dependents
SET t.fanIn = dependents
RETURN t.fqn AS type
Add fan-out to type
Now let’s add a property 'fanOut' to a Type with the number of other types it depends on.
MATCH (t:Type:File:Investigate)-[:DEPENDS_ON]->(dependency:Type)
WITH t, count(dependency) AS dependencies
SET t.fanOut = dependencies
RETURN t.fqn AS Type, t.fanOut AS fanOut
ORDER BY fanOut DESC
Add default fan-out
We can also add a property 'fanOut' to all Types without fanOut property.
MATCH (t:Type:File)
WHERE NOT exists(t.fanOut)
SET t.fanOut = 0
RETURN t.fqn AS type
Add default fan-out
Next, add a property 'fanIn' to all Types without fanIn property.
MATCH (t:Type:File:Investigate)
WHERE NOT exists(t.fanIn)
SET t.fanIn = 0
RETURN t.fqn AS type
Add type-coupling
Let’s add a property typeCoupling
to a Type
as sum of fanIn
and fanOut
.
MATCH (t:Type:File:Investigate)
SET t.typeCoupling = t.fanIn + t.fanOut
RETURN t.fqn AS type, t.typeCoupling AS typeCoupling,
t.fanIn AS fanIn, t.fanOut AS fanOut
ORDER BY typeCoupling DESC, fanIn DESC
Add in-package fan-out
We can add a property 'inPackageFanOut' to a Type with the number of other types it depends on.
MATCH (p1:Package)-[:CONTAINS]->(t:Type:File:Investigate)-[:DEPENDS_ON]->
(dependency:Type)<-[:CONTAINS]-(p2:Package)
WHERE p1 = p2 AND NOT dependency.fqn CONTAINS '$'
WITH t, count(dependency) AS dependencies
SET t.inPackageFanOut = dependencies
RETURN t.fqn AS type, t.inPackageFanOut AS fanOut
ORDER BY fanOut DESC
Add in-package fan-in
In this query, we add a property inPackageFanIn
to a Type
with the number of other types it depends on.
MATCH (p1:Package)-[:CONTAINS]->(t:Type:File:Investigate)<-[:DEPENDS_ON]-
(dependency:Type)<-[:CONTAINS]-(p2:Package)
WHERE p1 = p2 AND NOT dependency.fqn CONTAINS '$'
WITH t, count(dependency) AS dependencies
SET t.inPackageFanIn = dependencies
RETURN t.fqn AS type, t.inPackageFanIn AS fanIn
ORDER BY fanIn DESC
Add type-in-package coupling
Now we add a property typeInPackageCoupling
to a Type
as sum of fanIn
and fanOut
.
MATCH (t:Type:File:Investigate)
SET t.typeInPackageCoupling = t.inPackageFanIn + t.inPackageFanOut
RETURN t.fqn AS type, t.typeInPackageCoupling AS typeCoupling,
t.inPackageFanIn AS fanIn, t.inPackageFanOut AS fanOut
ORDER BY typeCoupling DESC, fanIn DESC
Unit Tests: Validate Assertions
Unit tests should have one (logical) assert per test method. Because some methods of a mocking framework also count as asserts, we want to label them.
Mockito example:
Here is an example for Mockito to label all assertion methods with name "verify*" declared by "org.mockito.Mockito" with Junit4
and Assert
.
MATCH (assertType:Type)-[:DECLARES]->(assertMethod)
WHERE assertType.fqn = 'org.mockito.Mockito'
AND assertMethod.signature CONTAINS 'verify'
SET assertMethod:Junit4:Assert
RETURN assertMethod
jUnit example:
Also the org.junit.Assert.fail method counts as an assert too:
MATCH (assertType:Type)-[:DECLARES]->(assertMethod)
WHERE assertType.fqn = 'org.junit.Assert'
AND assertMethod.signature starts with 'void fail'
SET assertMethod:Junit4:Assert
RETURN assertMethod
Test Coverage
Test coverage is a wide field. There are lots of discussions about unit tests and test coverage.
There is a JaCoCo Plugin by Kontext E for importing JaCoCo test coverage results into the jQAssistant database. With all information in one database, you may define your test coverage rules (and exceptions from the rules) in a very flexible way.
One example based on methods and their complexity is that more complex methods need more test coverage because the probability for bugs is higher (as a rule of thumb).
Define Test Coverage Goals
So we define two ranges of method complexity based on the number of branches:
CREATE (medium:TestCoverageRange {complexity : 'medium', min : 4, max : 5, coverage : 80})
CREATE (high:TestCoverageRange {complexity : 'high', min : 6, max : 999999, coverage : 90})
RETURN medium, high
Find methods with too low coverage
Now we can find methods with a too low test coverage:
MATCH (tcr:TestCoverageRange)
WITH tcr.min AS mincomplexity, tcr.max AS maxcomplexity, tcr.coverage AS coveragethreshold
MATCH (cl:Jacoco:Class)--(m:Jacoco:Method)--(c:Jacoco:Counter {type: 'COMPLEXITY'})
WHERE c.missed + c.covered >= mincomplexity AND c.missed + c.covered <= maxcomplexity
WITH m AS method, cl.fqn AS fqn, m.signature AS signature,
c.missed + c.covered AS complexity, coveragethreshold
MATCH (m)--(branches:Jacoco:Counter {type: 'BRANCH'})
WHERE m = method
WITH *, branches.covered * 100 / (branches.covered + branches.missed) AS coverage
WHERE coverage < coveragethreshold
RETURN complexity, coveragethreshold, coverage, fqn, signature
ORDER BY complexity, coverage
Add exceptions from the rule
And add some exceptions from this rule:
-
Methods equals() and hashCode() are generated by an IDE and need not to be tested
-
For some reason, we don’t want measure test coverage for the UI package
-
The
StringTool.doSomethingwithStrings
method should also be excluded -
And we know that there are 10 other violations that we want to skip for now + (but we swear to handle this Technical Debt in the next spring)
Query to add exceptions:
MATCH (tcr:TestCoverageRange)
WITH tcr.min AS mincomplexity, tcr.max AS maxcomplexity, tcr.coverage AS coveragethreshold
MATCH (cl:Jacoco:Class)--(m:Jacoco:Method)--(c:Jacoco:Counter {type: 'COMPLEXITY'})
WHERE c.missed + c.covered >= mincomplexity AND c.missed + c.covered <= maxcomplexity
AND NOT m.signature IN ['boolean equals(java.lang.Object)', 'int hashCode()']
AND NOT(cl.fqn STARTS WITH 'de.kontext_e.demo.ui')
AND NOT(cl.fqn = 'de.kontext_e.demo.tools.StringTool'
AND m.signature = 'java.lang.String doSomethingwithStrings(java.lang.String)')
WITH m AS method, cl.fqn AS fqn, m.signature AS signature, c.missed+c.covered AS complexity, coveragethreshold AS coveragethreshold
MATCH (m)--(branches:Jacoco:Counter {type: 'BRANCH'})
WHERE m=method AND branches.covered*100/(branches.covered+branches.missed) < coveragethreshold
RETURN complexity, coveragethreshold, branches.covered*100/(branches.covered+branches.missed) AS coverage, fqn, signature
ORDER BY complexity, coverage
SKIP 10
Special case: Frequently changed classes
Maybe it is also a good idea to have a higher test coverage for frequently changed classes. Using the Git Plugin by Kontext E, there is a way to test this:
MATCH (c:Git:Commit)-[:CONTAINS_CHANGE]->(change:Git:Change)-[:MODIFIES]->(f:Git:File)
WHERE f.relativePath=~'.*.java'
AND NOT f.relativePath CONTAINS 'ui'
WITH count(c) AS cnt, replace(f.relativePath, '/','.') AS gitfqn
ORDER BY cnt DESC
LIMIT 10
MATCH (class:Java:Class)
WHERE gitfqn CONTAINS class.fqn
WITH cnt, class.fqn AS classfqn
MATCH (cl:Jacoco:Class)--(m:Jacoco:Method)--(c:Jacoco:Counter {type: 'COMPLEXITY'})
WHERE classfqn=cl.fqn
AND c.missed+c.covered > 3
AND NOT(m.signature ='boolean equals(java.lang.Object)')
AND NOT(m.signature ='int hashCode()')
WITH m AS method, cl.fqn AS fqn, m.signature AS signature, c.missed+c.covered AS complexity
MATCH (m)--(branches:Jacoco:Counter {type: 'BRANCH'})
WHERE m=method
AND branches.covered*100/(branches.covered+branches.missed) < 90
RETURN DISTINCT fqn, signature, complexity, branches.covered*100/(branches.covered+branches.missed) AS coverage
ORDER BY fqn
SKIP 3
For the 10 most often changed Java files (except the ones in the UI package), the test coverage for branches should not be lower than 90 percent for methods with more than 3 branches - with three unnamed exceptions from this rule.
Encapsulation: Label types with internal FQNs as Internal
MATCH (t:Type) WHERE t.fqn CONTAINS {fqn_internal}
SET t:Internal
API/SPI types must not extend/implement internal types
MATCH (class:Class)-[:EXTENDS|IMPLEMENTS]->(supertype:Type:Internal)
WHERE NOT class:Internal
RETURN DISTINCT class as extendsInternal
API/SPI methods must not expose internal types
// return values
MATCH (class:Type)-[:DECLARES]->(method:Method)
WHERE NOT class:Internal
AND method.visibility IN ["public","protected"]
AND (exists ((method)-[:RETURNS]->(:Type:Internal)) OR
exists ((method)-[:`HAS`]->(:Parameter)-[:OF_TYPE]->(:Internal)))
RETURN method
API/SPI fields must not expose internal types
MATCH (class:Class:Internal)-[:DECLARES]->(field)-[:OF_TYPE]->(fieldtype:Type:Internal)
WHERE field.visibility IN ["public","protected"]
RETURN class as internalClass, field, fieldtype as internalType
Is this page helpful?