Skip to content

Commit 2a00f36

Browse files
authored
add hamcrest integration (via allure-framework#642)
1 parent 4a927db commit 2a00f36

11 files changed

Lines changed: 710 additions & 0 deletions

allure-hamcrest/build.gradle.kts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
description = "Allure Hamcrest Assertions Integration"
2+
3+
val agent: Configuration by configurations.creating
4+
5+
dependencies {
6+
agent("org.aspectj:aspectjweaver")
7+
api(project(":allure-java-commons"))
8+
compileOnly("org.aspectj:aspectjrt")
9+
implementation("org.hamcrest:hamcrest")
10+
testAnnotationProcessor(project(":allure-descriptions-javadoc"))
11+
testImplementation("org.junit.jupiter:junit-jupiter-api")
12+
testImplementation("org.junit.jupiter:junit-jupiter-params")
13+
testImplementation("org.assertj:assertj-core")
14+
testImplementation("org.slf4j:slf4j-simple")
15+
testImplementation(project(":allure-java-commons-test"))
16+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
17+
}
18+
19+
tasks.jar {
20+
manifest {
21+
attributes(mapOf(
22+
"Automatic-Module-Name" to "io.qameta.allure.hamcrest"
23+
))
24+
}
25+
}
26+
27+
tasks.test {
28+
useJUnitPlatform()
29+
doFirst {
30+
jvmArgs("-javaagent:${agent.singleFile}")
31+
}
32+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2021 Qameta Software OÜ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.hamcrest;
17+
18+
import org.aspectj.lang.JoinPoint;
19+
import org.aspectj.lang.annotation.Aspect;
20+
import org.aspectj.lang.annotation.Pointcut;
21+
import org.aspectj.lang.annotation.Before;
22+
import org.aspectj.lang.annotation.AfterThrowing;
23+
import org.aspectj.lang.annotation.AfterReturning;
24+
import io.qameta.allure.Allure;
25+
import io.qameta.allure.AllureLifecycle;
26+
import io.qameta.allure.model.Status;
27+
import io.qameta.allure.model.StepResult;
28+
import io.qameta.allure.util.ObjectUtils;
29+
import org.hamcrest.Matcher;
30+
import org.hamcrest.StringDescription;
31+
32+
import java.util.UUID;
33+
34+
import static io.qameta.allure.util.ResultsUtils.getStatus;
35+
36+
/**
37+
* <p>
38+
* Aspect "interceptor" for automatic logging to the Allure report.
39+
* </p>
40+
* <p>
41+
* This aspect should work for all asserts that are in the Hamcrest library, since they all go through a single method
42+
* to start the comparison.
43+
* </p>
44+
* <p>
45+
* In addition to the standard comparisons that are already in the Hamcrest library, this aspect should work correctly
46+
* with custom matchers if developers correctly implemented the describeMismatch and / or describeMismatchSafely
47+
* methods in the TypeSafeMatcher class.
48+
* </p>
49+
*
50+
* @author a-simeshin (Simeshin Artem)
51+
* @see org.hamcrest.TypeSafeMatcher
52+
*/
53+
@Aspect
54+
@SuppressWarnings("all")
55+
public class AllureHamcrestAssert {
56+
57+
private static InheritableThreadLocal<AllureLifecycle> lifecycle = new InheritableThreadLocal<AllureLifecycle>() {
58+
@Override
59+
protected AllureLifecycle initialValue() {
60+
return Allure.getLifecycle();
61+
}
62+
};
63+
64+
public static AllureLifecycle getLifecycle() {
65+
return lifecycle.get();
66+
}
67+
68+
@Pointcut("execution(void org.hamcrest.MatcherAssert.**(..))")
69+
public void initAssertThat() {
70+
}
71+
72+
/**
73+
* <p>
74+
* assertThat(String comment, Object actual, Matcher expected) - one and only one central entry point for all
75+
* asserts. Based on this rule, you only need to log a method with three arguments.
76+
* </p>
77+
* <p>
78+
* Even if there is no comment as the first argument, an empty string will be passed as the first argument.
79+
* </p>
80+
* <p>
81+
* For example assertThat(value123.get(), is(equalTo("value123"))) will be proxied to the metod
82+
* assertThat("", value123.get(), is(equalTo("value123")))
83+
* </p>
84+
*
85+
* @param joinPoint - entry point with args and method name
86+
*/
87+
@Before("initAssertThat()")
88+
public void catchAndStartStep(final JoinPoint joinPoint) {
89+
if (joinPoint.getArgs().length == 3) {
90+
final String reason = (String) joinPoint.getArgs()[0];
91+
final String actual = ObjectUtils.toString(joinPoint.getArgs()[1]);
92+
final StringDescription description = new StringDescription();
93+
final String expecting = description.appendText("assert \"")
94+
.appendText(actual)
95+
.appendText("\" ")
96+
.appendDescriptionOf((Matcher) joinPoint.getArgs()[2])
97+
.toString();
98+
99+
getLifecycle().startStep(
100+
UUID.randomUUID().toString(),
101+
new StepResult()
102+
.setName(reason.isEmpty() ? expecting : expecting + " | " + reason)
103+
.setDescription("Hamcrest assert")
104+
.setStatus(Status.PASSED)
105+
);
106+
}
107+
}
108+
109+
@AfterThrowing(pointcut = "initAssertThat()", throwing = "e")
110+
public void stepFailed(final Throwable e) {
111+
getLifecycle().updateStep(s -> s.setStatus(getStatus(e).orElse(Status.BROKEN)));
112+
getLifecycle().stopStep();
113+
}
114+
115+
@AfterReturning(pointcut = "initAssertThat()")
116+
public void stepStop() {
117+
getLifecycle().updateStep(s -> s.setStatus(Status.PASSED));
118+
getLifecycle().stopStep();
119+
}
120+
121+
/**
122+
* For tests only.
123+
*
124+
* @param allure allure lifecycle to set
125+
*/
126+
public static void setLifecycle(final AllureLifecycle allure) {
127+
lifecycle.set(allure);
128+
}
129+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<aspectj>
2+
<weaver options="-warn:none -Xlint:ignore"/>
3+
<aspects>
4+
<aspect name="io.qameta.allure.hamcrest.AllureHamcrestAssert"/>
5+
</aspects>
6+
</aspectj>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2021 Qameta Software OÜ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.hamcrest;
17+
18+
import io.qameta.allure.model.StepResult;
19+
import io.qameta.allure.model.TestResult;
20+
import org.assertj.core.api.Assertions;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.TestInstance;
23+
24+
import static io.qameta.allure.test.RunUtils.runWithinTestContext;
25+
import static org.hamcrest.MatcherAssert.assertThat;
26+
import static org.hamcrest.Matchers.equalToIgnoringCase;
27+
28+
/**
29+
* This tests should cover cases when reason string exists in assertion.
30+
*/
31+
@SuppressWarnings("all")
32+
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
33+
public class AllureHamcrestAssertionNameContainsReasonTest {
34+
35+
@Test
36+
void hamcrestAssertNameWithComment() {
37+
final TestResult testResult = runWithinTestContext(
38+
() -> assertThat(
39+
"Business always likes something weird",
40+
"TheBiscuit",
41+
equalToIgnoringCase("thebiscuit")
42+
),
43+
AllureHamcrestAssert::setLifecycle
44+
).getTestResults().get(0);
45+
46+
Assertions.assertThat(testResult.getSteps())
47+
.flatExtracting(StepResult::getName)
48+
.containsExactly("assert \"TheBiscuit\" a string equal to \"thebiscuit\" ignoring case | Business always likes something weird");
49+
}
50+
51+
@Test
52+
void hamcrestAssertNameWoComment() {
53+
final TestResult testResult = runWithinTestContext(
54+
() -> assertThat(
55+
"TheBiscuit",
56+
equalToIgnoringCase("thebiscuit")
57+
),
58+
AllureHamcrestAssert::setLifecycle
59+
).getTestResults().get(0);
60+
61+
Assertions.assertThat(testResult.getSteps())
62+
.flatExtracting(StepResult::getName)
63+
.containsExactly("assert \"TheBiscuit\" a string equal to \"thebiscuit\" ignoring case");
64+
}
65+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2021 Qameta Software OÜ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.hamcrest;
17+
18+
import io.qameta.allure.model.StepResult;
19+
import io.qameta.allure.model.TestResult;
20+
import org.assertj.core.api.Assertions;
21+
import org.hamcrest.Matcher;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.TestInstance;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.Arguments;
26+
import org.junit.jupiter.params.provider.MethodSource;
27+
28+
import java.util.Arrays;
29+
import java.util.HashMap;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.stream.Stream;
33+
34+
import static io.qameta.allure.test.RunUtils.runWithinTestContext;
35+
import static org.hamcrest.MatcherAssert.assertThat;
36+
import static org.hamcrest.Matchers.*;
37+
38+
/**
39+
* All tests should cover http://hamcrest.org/JavaHamcrest/tutorial "Collections" section
40+
* <ui>
41+
* <li>array</li>
42+
* <li>hasEntry</li>
43+
* <li>hasKey</li>
44+
* <li>hasValue</li>
45+
* <li>hasItem</li>
46+
* <li>hasItems</li>
47+
* <li>hasItemInArray</li>
48+
* </ui>
49+
*/
50+
@SuppressWarnings("all")
51+
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
52+
public class AllureHamcrestCollectionsMatchersTest {
53+
54+
@Test
55+
void hamcrestAssertNameForArrayMatchers() {
56+
final TestResult testResult = runWithinTestContext(
57+
() -> assertThat(new Integer[]{1,2,3}, is(array(equalTo(1), equalTo(2), equalTo(3)))),
58+
AllureHamcrestAssert::setLifecycle
59+
).getTestResults().get(0);
60+
61+
Assertions.assertThat(testResult.getSteps())
62+
.flatExtracting(StepResult::getName)
63+
.containsExactly("assert \"[1, 2, 3]\" is [<1>, <2>, <3>]");
64+
}
65+
66+
private static Stream<Arguments> mapTestCases() {
67+
HashMap<String, Integer> map = new HashMap<String, Integer>();
68+
map.put("key1", 1);
69+
map.put("key2", 2);
70+
map.put("key3", 3);
71+
72+
return Stream.of(
73+
Arguments.of(
74+
map, hasEntry(equalTo("key1"), equalTo(1)),
75+
"assert \"{key1=1, key2=2, key3=3}\" map containing [\"key1\"-><1>]"
76+
),
77+
Arguments.of(
78+
map, hasKey(equalTo("key2")),
79+
"assert \"{key1=1, key2=2, key3=3}\" map containing [\"key2\"->ANYTHING]"
80+
),
81+
Arguments.of(
82+
map, hasValue(equalTo(3)),
83+
"assert \"{key1=1, key2=2, key3=3}\" map containing [ANYTHING-><3>]"
84+
)
85+
);
86+
}
87+
88+
@ParameterizedTest
89+
@MethodSource("mapTestCases")
90+
void hamcrestAssertNameForMapMatchers(Map actual, Matcher matcher, String expectedName) {
91+
final TestResult testResult = runWithinTestContext(
92+
() -> assertThat(actual, matcher),
93+
AllureHamcrestAssert::setLifecycle
94+
).getTestResults().get(0);
95+
96+
Assertions.assertThat(testResult.getSteps())
97+
.flatExtracting(StepResult::getName)
98+
.containsExactly(expectedName);
99+
}
100+
101+
private static Stream<Arguments> iterableTestCases() {
102+
List<String> list = Arrays.asList("val1", "val2", "val3");
103+
104+
return Stream.of(
105+
Arguments.of(
106+
list, hasItem(equalTo("key1")),
107+
"assert \"[val1, val2, val3]\" a collection containing \"key1\""
108+
),
109+
Arguments.of(
110+
list, hasItems(startsWith("v"), endsWith("l2")),
111+
"assert \"[val1, val2, val3]\" (a collection containing a string starting with \"v\" and a collection containing a string ending with \"l2\")"
112+
),
113+
Arguments.of(
114+
list, hasItemInArray(Arrays.asList("val1", "val2")),
115+
"assert \"[val1, val2, val3]\" an array containing <[val1, val2]>"
116+
)
117+
);
118+
}
119+
120+
@ParameterizedTest
121+
@MethodSource("iterableTestCases")
122+
void hamcrestAssertNameForIterableMatchers(Iterable actual, Matcher matcher, String expectedName) {
123+
final TestResult testResult = runWithinTestContext(
124+
() -> assertThat(actual, matcher),
125+
AllureHamcrestAssert::setLifecycle
126+
).getTestResults().get(0);
127+
128+
Assertions.assertThat(testResult.getSteps())
129+
.flatExtracting(StepResult::getName)
130+
.containsExactly(expectedName);
131+
}
132+
}

0 commit comments

Comments
 (0)