GraphGists 图示

jQAssistant 演示指南

jqa

简介

jQAssistant 是一款 QA 工具,允许在结构层面定义和验证项目特定的规则。它基于 Neo4j 图数据库构建,可以轻松集成到构建过程中,以自动化检测约束违规并生成关于用户定义概念和指标的报告。

示例用例

  • 强制执行命名规范(例如 EJBs, JPA 实体, 测试类, 包, Maven 模块等)

  • 验证项目模块间的依赖关系

  • 分离 API 和实现包

  • 检测常见问题,如循环依赖或没有断言的测试

使用 jQAssistant

规则以 Cypher 表达 - 这是 Neo4j 易学的查询语言

没有调用 assert 方法的测试
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

分析:项目包含多少种类型?

此查询计算所有带有 TypeFile 标签的节点,即直接从文件中扫描的所有 Java 类型。

MATCH (t:Type:File)
RETURN count(t)

分析:哪个类继承自另一个类?

此查询显示前 20 个类的 FQNs 及其超类型(类或接口)。

MATCH (c1:Class)-[:EXTENDS]->(c2:Type)
RETURN c1.fqn, c2.fqn
LIMIT 20

分析:哪些类包含最多的方法?

每个 Type DECLARES 成员,即链接到 MethodField 节点。这里我们只计算方法并返回前 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

可见性:查找不必要的公共可见性

第一步:给公共方法打上“Public”标签。

MATCH (m:Method)
WHERE  m.visibility='public'
 SET m:Public

可见性:第二步

第二步 - 报告从同一包内调用的前 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,作为 fanInfanOut 的总和。

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,作为 fanInfanOut 的总和。

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*" 的所有断言方法标记为 Junit4Assert

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% - 此规则有三个未命名的例外。

封装:将具有内部 FQNs 的类型标记为 Internal

MATCH (t:Type) WHERE t.fqn CONTAINS {fqn_internal}
SET t:Internal

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
© . All rights reserved.