jQAssistant 演示指南
简介
jQAssistant 是一款 QA 工具,允许在结构层面定义和验证项目特定的规则。它基于 Neo4j 图数据库构建,可以轻松集成到构建过程中,以自动化检测约束违规并生成关于用户定义概念和指标的报告。
示例用例
-
强制执行命名规范(例如 EJBs, JPA 实体, 测试类, 包, Maven 模块等)
-
验证项目模块间的依赖关系
-
分离 API 和实现包
-
检测常见问题,如循环依赖或没有断言的测试
使用 jQAssistant
规则以 Cypher 表达 - 这是 Neo4j 易学的查询语言
MATCH (t:Test:Method)
WHERE NOT (t)-[:INVOKES]->(:Assert:Method)
RETURN t AS TestWithoutAssertion
许可:jQAssistant 在 GNU 通用公共许可协议 v3 下贡献。
扫描过程
如果您尚未扫描项目,请在此处下载 jQAssistant。
然后使用此命令运行项目扫描
bin/jqassistant.sh scan -f lib
它将扫描以下方面
-
Java 类文件和 Jar 文件
-
Maven pom.xml
-
XML 配置文件
-
Spring 配置
-
Persistence.xml
-
……以及更多
启动 Neo4j 并运行 jqa
然后,您可以使用以下命令启动 Neo4j 服务器
bin/jqassistant.sh server
并在该服务器内使用 :play https://guides.neo4j.com/jqa
运行此指南。
数据概览
如果您的 Neo4j 实例正在扫描的数据上运行,您可以使用此查询快速了解数据模型
call db.schema()
如需表格概览,请使用此查询
MATCH (n)
// a collection of multiple labels is turned into rows
UNWIND labels(n) as label
RETURN label, count(*)
ORDER BY count(*) asc
分析:哪个类继承自另一个类?
此查询显示前 20 个类的 FQNs 及其超类型(类或接口)。
MATCH (c1:Class)-[:EXTENDS]->(c2:Type)
RETURN c1.fqn, c2.fqn
LIMIT 20
分析:哪些类包含最多的方法?
每个 Type DECLARES
成员,即链接到 Method
或 Field
节点。这里我们只计算方法并返回前 20 个方法数量最多的类。
MATCH (class:Class)-[:DECLARES]->(method:Method)
RETURN class.fqn, count(method) AS Methods
ORDER BY Methods DESC
LIMIT 20
分析:哪个类具有最深的继承层级?
我们传递性地跟踪 EXTENDS 关系直到层级顶部,并返回前 20 个最长的继承链。
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
分析:哪些类受到特定异常的影响?
现在我们想知道哪些方法传递性地调用了给定异常类型的构造函数。
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
分析:给定包中有多少方法调用了某个东西?
了解更改方法返回类型会影响多少方法,或者解耦某些架构构件需要多少精力,这会很有趣。
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
可见性:第二步
第二步 - 报告从同一包内调用的前 20 个公共方法。
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
不变性:将状态不可变的类标记为“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
进一步分析:标记需要调查的类型
标记某个包中的类型以便进行调查。这样,我们就不必总是检查此条件:WHERE exists(t.byteCodeVersion) AND t.fqn STARTS WITH 'some.package.'
,而只需匹配 :Investigate
标签即可。
MATCH (t:Type:File)<-[:DEPENDS_ON]-(dependent:Type)
WHERE exists(t.byteCodeVersion) AND t.fqn STARTS WITH {package}
SET t:Investigate
进一步分析:向类型添加扇入度
让我们向一个 Type 添加属性 'fanIn',表示依赖于它的其他类型的数量。
MATCH (t:Type:File:Investigate)<-[:DEPENDS_ON]-(dependent:Type)
WITH t, count(dependent) AS dependents
SET t.fanIn = dependents
RETURN t.fqn AS type
向类型添加扇出度
现在,让我们向一个 Type 添加属性 'fanOut',表示它依赖于的其他类型的数量。
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
添加默认扇出度
我们还可以向所有没有 fanOut 属性的 Type 添加属性 'fanOut'。
MATCH (t:Type:File)
WHERE NOT exists(t.fanOut)
SET t.fanOut = 0
RETURN t.fqn AS type
添加默认扇出度
接下来,向所有没有 fanIn 属性的 Type 添加属性 'fanIn'。
MATCH (t:Type:File:Investigate)
WHERE NOT exists(t.fanIn)
SET t.fanIn = 0
RETURN t.fqn AS type
添加类型耦合度
让我们向一个 Type
添加属性 typeCoupling
,作为 fanIn
和 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
添加包内扇出度
我们可以向一个 Type 添加属性 'inPackageFanOut',表示它依赖于的其他类型的数量。
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
添加包内扇入度
在此查询中,我们向一个 Type
添加属性 inPackageFanIn
,表示依赖于它的其他类型的数量。
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
添加类型包内耦合度
现在,我们向一个 Type
添加属性 typeInPackageCoupling
,作为 fanIn
和 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
Mockito 示例
这是一个 Mockito 示例,将由 "org.mockito.Mockito" 声明的名称为 "verify*" 的所有断言方法标记为 Junit4
和 Assert
。
MATCH (assertType:Type)-[:DECLARES]->(assertMethod)
WHERE assertType.fqn = 'org.mockito.Mockito'
AND assertMethod.signature CONTAINS 'verify'
SET assertMethod:Junit4:Assert
RETURN assertMethod
jUnit 示例
另外 org.junit.Assert.fail 方法也算作一个断言。
MATCH (assertType:Type)-[:DECLARES]->(assertMethod)
WHERE assertType.fqn = 'org.junit.Assert'
AND assertMethod.signature starts with 'void fail'
SET assertMethod:Junit4:Assert
RETURN assertMethod
测试覆盖率
测试覆盖率是一个广泛的领域。关于单元测试和测试覆盖率有很多讨论。
有一个 Kontext E 提供的 JaCoCo 插件,用于将 JaCoCo 测试覆盖率结果导入 jQAssistant 数据库。将所有信息集中在一个数据库中,您可以非常灵活地定义您的测试覆盖率规则(以及规则例外)。
基于方法及其复杂性的一个例子是,更复杂的方法需要更高的测试覆盖率,因为出现 bug 的可能性更高(作为经验法则)。
定义测试覆盖率目标
因此,我们根据分支数量定义了两种方法复杂性范围
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
查找覆盖率过低的方法
现在我们可以找到测试覆盖率过低的方法
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
添加规则例外
并为此规则添加一些例外
-
equals() 和 hashCode() 方法由 IDE 生成,无需测试
-
由于某些原因,我们不希望测量 UI 包的测试覆盖率
-
StringTool.doSomethingwithStrings
方法也应被排除在外 -
我们知道还有 10 个其他违规项,我们暂时跳过(但我们发誓在明年春天处理这些技术债务)
添加例外的查询
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
特殊情况:频繁更改的类
也许为频繁更改的类设置更高的测试覆盖率也是个好主意。使用 Kontext E 提供的 Git 插件,有办法进行此测试
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
对于更改最频繁的 10 个 Java 文件(UI 包中的文件除外),分支数超过 3 的方法的测试分支覆盖率不应低于 90% - 此规则有三个未命名的例外。
API/SPI 类型不得继承/实现内部类型
MATCH (class:Class)-[:EXTENDS|IMPLEMENTS]->(supertype:Type:Internal)
WHERE NOT class:Internal
RETURN DISTINCT class as extendsInternal
API/SPI 方法不得暴露内部类型
// 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 字段不得暴露内部类型
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
此页面有帮助吗?