jQAssistant 演示指南
简介
jQAssistant 是一款 QA 工具,它允许在结构层面上定义和验证项目特定的规则。它建立在 Neo4j 图数据库之上,可以轻松地集成到构建流程中,以自动检测约束违规并生成有关用户定义的概念和指标的报告。
示例用例
-
强制命名约定(例如 EJB、JPA 实体、测试类、包、Maven 模块等)
-
验证项目模块之间的依赖关系
-
分离 API 和实现包
-
检测常见问题,例如循环依赖或没有断言的测试
使用 jQAssistant
规则用 Cypher 表达,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 个类的 FQN 以及它们的超类型(类或接口)。
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
可见性:步骤 2
第二步 - 报告在同一包内调用的前 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
进一步分析:标记要调查的类型
标记要调查的某个包中的类型。因此,我们可以直接匹配 :Investigate
标签,而不是始终检查以下条件:WHERE exists(t.byteCodeVersion) AND t.fqn STARTS WITH 'some.package.'
。
MATCH (t:Type:File)<-[:DEPENDS_ON]-(dependent:Type)
WHERE exists(t.byteCodeVersion) AND t.fqn STARTS WITH {package}
SET t:Investigate
进一步分析:将扇入添加到类型
让我们将属性 'fanIn' 添加到 Type,其中包含依赖它的其他类型的数量。
MATCH (t:Type:File:Investigate)<-[:DEPENDS_ON]-(dependent:Type)
WITH t, count(dependent) AS dependents
SET t.fanIn = dependents
RETURN t.fqn AS type
添加扇出到类型
现在让我们将属性 'fanOut' 添加到 Type,其中包含它依赖它的其他类型的数量。
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' 添加到所有没有 fanOut 属性的 Type。
MATCH (t:Type:File)
WHERE NOT exists(t.fanOut)
SET t.fanOut = 0
RETURN t.fqn AS type
添加默认扇出
接下来,将属性 'fanIn' 添加到所有没有 fanIn 属性的 Type。
MATCH (t:Type:File:Investigate)
WHERE NOT exists(t.fanIn)
SET t.fanIn = 0
RETURN t.fqn AS type
添加类型耦合
让我们将属性 typeCoupling
添加到 Type
,作为 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
添加包内扇出
我们可以将属性 'inPackageFanOut' 添加到 Type,其中包含它依赖它的其他类型的数量。
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
添加包内扇入
在此查询中,我们将属性 inPackageFanIn
添加到 Type
,其中包含它依赖它的其他类型的数量。
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
添加包内类型耦合
现在我们添加一个属性 typeInPackageCoupling
到 Type
,作为 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 示例,用于使用 Junit4
和 Assert
将所有由“org.mockito.Mockito”声明的名称为“verify*”的断言方法标记为 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 数据库。将所有信息保存在一个数据库中,你就可以以非常灵活的方式定义测试覆盖率规则(以及规则的例外)。
一个基于方法及其复杂度的示例是,更复杂的方法需要更多的测试覆盖率,因为出现错误的可能性更高(作为经验法则)。
定义测试覆盖率目标
因此,我们根据分支数量定义了两个方法复杂度范围
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
此页面是否有帮助?