Skip to content

Commit f04dde7

Browse files
author
Nick Grippin
authored
Merge pull request cfpb#1111 from schbetsy/test-utils
Test race, ethnicity, and minority status utilities
2 parents 2d5850e + 75efecb commit f04dde7

File tree

8 files changed

+472
-37
lines changed

8 files changed

+472
-37
lines changed

publication/src/main/scala/hmda/publication/reports/util/EthnicityUtil.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package hmda.publication.reports.util
22

33
import akka.NotUsed
44
import akka.stream.scaladsl.Source
5-
import hmda.model.publication.reports.{ ApplicantIncomeEnum, EthnicityBorrowerCharacteristic, EthnicityCharacteristic, EthnicityEnum }
5+
import hmda.model.publication.reports.{ EthnicityBorrowerCharacteristic, EthnicityCharacteristic, EthnicityEnum }
66
import hmda.model.publication.reports.EthnicityEnum._
77
import hmda.publication.reports.util.DispositionType.DispositionType
88
import hmda.publication.reports.util.ReportUtil.calculateDispositions
@@ -23,11 +23,11 @@ object EthnicityUtil {
2323
lar.ethnicity == 2 &&
2424
(lar.coEthnicity == 2 || coapplicantEthnicityNotProvided(lar))
2525
}
26-
case NotAvailable => larSource.filter { lar =>
26+
case Joint => larSource.filter { lar =>
2727
(lar.ethnicity == 1 && lar.coEthnicity == 2) ||
2828
(lar.ethnicity == 2 && lar.coEthnicity == 1)
2929
}
30-
case Joint => larSource.filter { lar =>
30+
case NotAvailable => larSource.filter { lar =>
3131
lar.ethnicity == 3 || lar.ethnicity == 4
3232
}
3333
}
@@ -41,7 +41,6 @@ object EthnicityUtil {
4141

4242
def ethnicityBorrowerCharacteristic[as: AS, mat: MAT, ec: EC](
4343
larSource: Source[LoanApplicationRegisterQuery, NotUsed],
44-
applicantIncomeEnum: ApplicantIncomeEnum,
4544
dispositions: List[DispositionType]
4645
): Future[EthnicityBorrowerCharacteristic] = {
4746

publication/src/main/scala/hmda/publication/reports/util/MinorityStatusUtil.scala

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,16 @@ import scala.concurrent.Future
1414
object MinorityStatusUtil {
1515

1616
def filterMinorityStatus(larSource: Source[LoanApplicationRegisterQuery, NotUsed], minorityStatus: MinorityStatusEnum): Source[LoanApplicationRegisterQuery, NotUsed] = {
17-
val relevantLars = larSource.filter { lar =>
18-
(lar.ethnicity == 2 && applicantRace1NotProvided(lar)) ||
19-
(applicantEthnicityNotProvided(lar) && lar.race1 == 5) ||
20-
(applicantEthnicityNotProvided(lar) && applicantRace1NotProvided(lar))
21-
}
22-
2317
minorityStatus match {
24-
case WhiteNonHispanic => relevantLars.filter { lar =>
25-
lar.ethnicity == 2 && lar.race1 == 1
18+
case WhiteNonHispanic => larSource.filter { lar =>
19+
lar.ethnicity == 2 && lar.race1 == 5
2620
}
27-
case OtherIncludingHispanic => relevantLars.filter { lar =>
21+
case OtherIncludingHispanic => larSource.filter { lar =>
2822
lar.ethnicity == 1 && applicantRacesAllNonWhite(lar)
2923
}
3024
}
3125
}
3226

33-
private def applicantEthnicityNotProvided(lar: LoanApplicationRegisterQuery): Boolean = {
34-
lar.ethnicity == 3 || lar.ethnicity == 4
35-
}
36-
37-
private def applicantRace1NotProvided(lar: LoanApplicationRegisterQuery): Boolean = {
38-
lar.race1 == 6 || lar.race1 == 7
39-
}
40-
4127
private def applicantRacesAllNonWhite(lar: LoanApplicationRegisterQuery): Boolean = {
4228
val race1NonWhite = lar.race1 == 1 || lar.race1 == 2 || lar.race1 == 3 || lar.race1 == 4
4329

@@ -50,7 +36,6 @@ object MinorityStatusUtil {
5036

5137
def minorityStatusBorrowerCharacteristic[as: AS, mat: MAT, ec: EC](
5238
larSource: Source[LoanApplicationRegisterQuery, NotUsed],
53-
applicantIncomeEnum: ApplicantIncomeEnum,
5439
dispositions: List[DispositionType]
5540
): Future[MinorityStatusBorrowerCharacteristic] = {
5641

publication/src/main/scala/hmda/publication/reports/util/RaceUtil.scala

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package hmda.publication.reports.util
22

33
import akka.NotUsed
44
import akka.stream.scaladsl.Source
5-
import hmda.model.publication.reports.{ ApplicantIncomeEnum, RaceBorrowerCharacteristic, RaceCharacteristic, RaceEnum }
5+
import hmda.model.publication.reports.{ RaceBorrowerCharacteristic, RaceCharacteristic, RaceEnum }
66
import hmda.model.publication.reports.RaceEnum._
77
import hmda.publication.reports._
88
import hmda.publication.reports.util.ReportUtil.calculateDispositions
@@ -49,8 +49,8 @@ object RaceUtil {
4949

5050
case Joint =>
5151
larSource.filter { lar =>
52-
(applicantTwoOrMoreMinorities(lar) || coApplicantTwoOrMoreMinorities(lar)) &&
53-
(lar.race1 == 5 || coApplicantWhite(lar))
52+
(applicantOneOrMoreMinorities(lar) || coApplicantOneOrMoreMinorities(lar)) &&
53+
(applicantWhite(lar) || coApplicantWhite(lar))
5454
}
5555

5656
case NotProvided =>
@@ -66,6 +66,14 @@ object RaceUtil {
6666
lar.race5 == ""
6767
}
6868

69+
private def applicantWhite(lar: LoanApplicationRegisterQuery): Boolean = {
70+
lar.race1 == 5 &&
71+
lar.race2 == "" &&
72+
lar.race3 == "" &&
73+
lar.race4 == "" &&
74+
lar.race5 == ""
75+
}
76+
6977
private def coApplicantWhite(lar: LoanApplicationRegisterQuery): Boolean = {
7078
lar.coRace1 == 5 &&
7179
lar.coRace2 == "" &&
@@ -99,15 +107,22 @@ object RaceUtil {
99107
(lar.race5 != "" && lar.race5 != "5"))
100108
}
101109

102-
private def coApplicantTwoOrMoreMinorities(lar: LoanApplicationRegisterQuery): Boolean = {
103-
lar.coRace1 != 5 &&
104-
((lar.coRace2 != "" && lar.coRace2 != "5") ||
105-
(lar.coRace3 != "" && lar.coRace3 != "5") ||
106-
(lar.coRace4 != "" && lar.coRace4 != "5") ||
107-
(lar.coRace5 != "" && lar.coRace5 != "5"))
110+
private def applicantOneOrMoreMinorities(lar: LoanApplicationRegisterQuery): Boolean = {
111+
(lar.race1 == 1 || lar.race1 == 2 || lar.race1 == 3 || lar.race1 == 4) ||
112+
(lar.race2 != "" && lar.race2 != "5") ||
113+
(lar.race3 != "" && lar.race3 != "5") ||
114+
(lar.race4 != "" && lar.race4 != "5") ||
115+
(lar.race5 != "" && lar.race5 != "5")
116+
}
117+
private def coApplicantOneOrMoreMinorities(lar: LoanApplicationRegisterQuery): Boolean = {
118+
(lar.coRace1 == 1 || lar.coRace1 == 2 || lar.coRace1 == 3 || lar.coRace1 == 4) ||
119+
(lar.coRace2 != "" && lar.coRace2 != "5") ||
120+
(lar.coRace3 != "" && lar.coRace3 != "5") ||
121+
(lar.coRace4 != "" && lar.coRace4 != "5") ||
122+
(lar.coRace5 != "" && lar.coRace5 != "5")
108123
}
109124

110-
def raceBorrowerCharacteristic[as: AS, mat: MAT, ec: EC](larSource: Source[LoanApplicationRegisterQuery, NotUsed], applicantIncomeEnum: ApplicantIncomeEnum, dispositions: List[DispositionType]): Future[RaceBorrowerCharacteristic] = {
125+
def raceBorrowerCharacteristic[as: AS, mat: MAT, ec: EC](larSource: Source[LoanApplicationRegisterQuery, NotUsed], dispositions: List[DispositionType]): Future[RaceBorrowerCharacteristic] = {
111126

112127
val larsAlaskan = filterRace(larSource, AmericanIndianOrAlaskaNative)
113128
val larsAsian = filterRace(larSource, Asian)

publication/src/main/scala/hmda/publication/reports/util/ReportUtil.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,17 @@ object ReportUtil extends SourceUtils {
5959
)
6060
}
6161

62-
def borrowerCharacteristicsByIncomeInterval[ec: EC, mat: MAT, as: AS](larsByIncome: Map[ApplicantIncomeEnum, Source[LoanApplicationRegisterQuery, NotUsed]], dispositions: List[DispositionType]): Map[ApplicantIncomeEnum, Future[List[BorrowerCharacteristic]]] = {
62+
def borrowerCharacteristicsByIncomeInterval[ec: EC, mat: MAT, as: AS](
63+
larsByIncome: Map[ApplicantIncomeEnum, Source[LoanApplicationRegisterQuery, NotUsed]],
64+
dispositions: List[DispositionType]
65+
): Map[ApplicantIncomeEnum, Future[List[BorrowerCharacteristic]]] = {
6366
larsByIncome.map {
6467
case (income, lars) =>
6568
val characteristics = Future.sequence(
6669
List(
67-
raceBorrowerCharacteristic(lars, income, dispositions),
68-
ethnicityBorrowerCharacteristic(lars, income, dispositions),
69-
minorityStatusBorrowerCharacteristic(lars, income, dispositions)
70+
raceBorrowerCharacteristic(lars, dispositions),
71+
ethnicityBorrowerCharacteristic(lars, dispositions),
72+
minorityStatusBorrowerCharacteristic(lars, dispositions)
7073
)
7174
)
7275
income -> characteristics
@@ -77,7 +80,10 @@ object ReportUtil extends SourceUtils {
7780
collectHeadValue(larSource).map(lar => lar.actionTakenDate.toString.substring(0, 4).toInt)
7881
}
7982

80-
def calculateDispositions[ec: EC, mat: MAT, as: AS](larSource: Source[LoanApplicationRegisterQuery, NotUsed], dispositions: List[DispositionType]): Future[List[Disposition]] = {
83+
def calculateDispositions[ec: EC, mat: MAT, as: AS](
84+
larSource: Source[LoanApplicationRegisterQuery, NotUsed],
85+
dispositions: List[DispositionType]
86+
): Future[List[Disposition]] = {
8187
Future.sequence(dispositions.map(_.calculateDisposition(larSource)))
8288
}
8389

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package hmda.publication.reports.util
2+
3+
import akka.NotUsed
4+
import akka.actor.ActorSystem
5+
import akka.stream.ActorMaterializer
6+
import akka.stream.scaladsl.Source
7+
import hmda.model.fi.lar.{ Applicant, LarGenerators, LoanApplicationRegister }
8+
import hmda.query.model.filing.LoanApplicationRegisterQuery
9+
import hmda.query.repository.filing.LarConverter._
10+
11+
trait ApplicantSpecUtil extends LarGenerators {
12+
13+
implicit val system = ActorSystem()
14+
implicit val ec = system.dispatcher
15+
implicit val materializer = ActorMaterializer()
16+
17+
def larCollectionWithApplicant(transformation: (Applicant => Applicant)): List[LoanApplicationRegister] = {
18+
lar100ListGen.sample.get.map { lar =>
19+
val newApplicant = transformation(lar.applicant)
20+
lar.copy(applicant = newApplicant)
21+
}
22+
}
23+
24+
def source(lars: List[LoanApplicationRegister]): Source[LoanApplicationRegisterQuery, NotUsed] = Source
25+
.fromIterator(() => lars.toIterator)
26+
.map(lar => toLoanApplicationRegisterQuery(lar))
27+
28+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package hmda.publication.reports.util
2+
3+
import hmda.model.publication.reports.ActionTakenTypeEnum.{ ApplicationReceived, LoansOriginated }
4+
import hmda.model.publication.reports.EthnicityEnum._
5+
import hmda.model.publication.reports.{ EthnicityBorrowerCharacteristic, EthnicityCharacteristic }
6+
import hmda.publication.reports.util.DispositionType.{ OriginatedDisp, ReceivedDisp }
7+
import hmda.publication.reports.util.EthnicityUtil._
8+
import hmda.util.SourceUtils
9+
import org.scalacheck.Gen
10+
import org.scalatest.{ AsyncWordSpec, MustMatchers }
11+
12+
class EthnicityUtilSpec extends AsyncWordSpec with MustMatchers with SourceUtils with ApplicantSpecUtil {
13+
14+
"'Hispanic or Latino' ethnicity filter" must {
15+
"include applications that meet 'Hispanic or Latino' criteria" in {
16+
def coAppEthnicity = Gen.oneOf(1, 3, 4, 5).sample.get
17+
val lars = larCollectionWithApplicant(_.copy(ethnicity = 1, coEthnicity = coAppEthnicity))
18+
val latinoLars = filterEthnicity(source(lars), HispanicOrLatino)
19+
count(latinoLars).map(_ mustBe 100)
20+
}
21+
"exclude applications where applicant does not meet criteria" in {
22+
val larsExcludedByApplicant = larCollectionWithApplicant(_.copy(ethnicity = 2, coEthnicity = 3))
23+
val nonLatinoLars1 = filterEthnicity(source(larsExcludedByApplicant), HispanicOrLatino)
24+
count(nonLatinoLars1).map(_ mustBe 0)
25+
}
26+
"exclude applications where coApplicant does not meet criteria" in {
27+
val larsExcludedByCoApplicant = larCollectionWithApplicant(_.copy(ethnicity = 1, coEthnicity = 2))
28+
val nonLatinoLars2 = filterEthnicity(source(larsExcludedByCoApplicant), HispanicOrLatino)
29+
count(nonLatinoLars2).map(_ mustBe 0)
30+
}
31+
}
32+
33+
"'Not Hispanic or Latino' ethnicity filter" must {
34+
"include applications that meet 'Not Hispanic/Latino' criteria" in {
35+
def coAppEthnicity = Gen.oneOf(2, 3, 4, 5).sample.get
36+
val lars = larCollectionWithApplicant(_.copy(ethnicity = 2, coEthnicity = coAppEthnicity))
37+
val nonLatinoLars = filterEthnicity(source(lars), NotHispanicOrLatino)
38+
count(nonLatinoLars).map(_ mustBe 100)
39+
}
40+
"exclude applications where applicant does not meet criteria" in {
41+
val larsExcludedByApplicant = larCollectionWithApplicant(_.copy(ethnicity = 1, coEthnicity = 3))
42+
val latinoLars1 = filterEthnicity(source(larsExcludedByApplicant), NotHispanicOrLatino)
43+
count(latinoLars1).map(_ mustBe 0)
44+
}
45+
"exclude applications where coApplicant does not meet criteria" in {
46+
val larsExcludedByCoApplicant = larCollectionWithApplicant(_.copy(ethnicity = 2, coEthnicity = 1))
47+
val latinoLars2 = filterEthnicity(source(larsExcludedByCoApplicant), NotHispanicOrLatino)
48+
count(latinoLars2).map(_ mustBe 0)
49+
}
50+
}
51+
52+
"'Not Available' ethnicity filter" must {
53+
"include applications that meet 'Not Available' criteria" in {
54+
def appEthnicity = Gen.oneOf(3, 4).sample.get
55+
val lars = larCollectionWithApplicant(_.copy(ethnicity = appEthnicity))
56+
57+
val notAvailableLars = filterEthnicity(source(lars), NotAvailable)
58+
count(notAvailableLars).map(_ mustBe 100)
59+
}
60+
"exclude applications that do not meet 'Not Available' criteria" in {
61+
def appEthnicity = Gen.oneOf(1, 2).sample.get
62+
val larsExcludedByApplicant = larCollectionWithApplicant(_.copy(ethnicity = appEthnicity))
63+
val lars = filterEthnicity(source(larsExcludedByApplicant), NotAvailable)
64+
count(lars).map(_ mustBe 0)
65+
}
66+
}
67+
68+
"'Joint' ethnicity filter" must {
69+
"include applications with hispanic applicant and non-hispanic coApplicant" in {
70+
val lars1 = larCollectionWithApplicant(_.copy(ethnicity = 1, coEthnicity = 2))
71+
val jointLars1 = filterEthnicity(source(lars1), Joint)
72+
count(jointLars1).map(_ mustBe 100)
73+
}
74+
"include applications with non-hispanic applicant and hispanic coApplicant" in {
75+
val lars2 = larCollectionWithApplicant(_.copy(ethnicity = 2, coEthnicity = 1))
76+
val jointLars2 = filterEthnicity(source(lars2), Joint)
77+
count(jointLars2).map(_ mustBe 100)
78+
}
79+
"exclude applications that do not meet 'Joint' criteria" in {
80+
def ethnicity = Gen.oneOf(1, 2).sample.get
81+
val larsWithSameEthnicity = larCollectionWithApplicant { app =>
82+
val eth = ethnicity
83+
app.copy(ethnicity = eth, coEthnicity = eth)
84+
}
85+
val lars = filterEthnicity(source(larsWithSameEthnicity), Joint)
86+
count(lars).map(_ mustBe 0)
87+
}
88+
}
89+
90+
"ethnicityBorrowerCharacteristic" must {
91+
"generate a EthnicityBorrowCharacteristic with all 4 ethnicity categories and the specified dispositions" in {
92+
val lars = lar100ListGen.sample.get
93+
val dispositions = List(ReceivedDisp, OriginatedDisp)
94+
95+
val resultF = ethnicityBorrowerCharacteristic(source(lars), dispositions)
96+
97+
resultF.map { result =>
98+
result mustBe a[EthnicityBorrowerCharacteristic]
99+
100+
result.ethnicities.size mustBe 4
101+
102+
val firstEthCharacteristic = result.ethnicities.head
103+
firstEthCharacteristic mustBe a[EthnicityCharacteristic]
104+
firstEthCharacteristic.ethnicity mustBe HispanicOrLatino
105+
firstEthCharacteristic.dispositions.map(_.disposition) mustBe List(ApplicationReceived, LoansOriginated)
106+
}
107+
}
108+
}
109+
110+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package hmda.publication.reports.util
2+
3+
import hmda.model.publication.reports.ActionTakenTypeEnum.{ ApplicationReceived, LoansOriginated }
4+
import hmda.model.publication.reports.MinorityStatusEnum._
5+
import hmda.model.publication.reports.{ MinorityStatusBorrowerCharacteristic, MinorityStatusCharacteristic }
6+
import hmda.publication.reports.util.DispositionType.{ OriginatedDisp, ReceivedDisp }
7+
import hmda.publication.reports.util.MinorityStatusUtil._
8+
import hmda.util.SourceUtils
9+
import org.scalacheck.Gen
10+
import org.scalatest.{ AsyncWordSpec, MustMatchers }
11+
12+
class MinorityStatusUtilSpec extends AsyncWordSpec with MustMatchers with SourceUtils with ApplicantSpecUtil {
13+
14+
"'White Non-Hispanic' minority status filter" must {
15+
"include applications that meet 'White Non-Hispanic' criteria" in {
16+
val lars = larCollectionWithApplicant(_.copy(ethnicity = 2, race1 = 5))
17+
val nonHispanicLars = filterMinorityStatus(source(lars), WhiteNonHispanic)
18+
count(nonHispanicLars).map(_ mustBe 100)
19+
}
20+
"exclude applications that do not meet 'White Non-Hispanic' criteria" in {
21+
def excludedRace = Gen.oneOf(1, 2, 3, 4, 6, 7).sample.get
22+
def excludedEthnicity = Gen.oneOf(1, 3, 4).sample.get
23+
24+
val excludedLars = larCollectionWithApplicant(_.copy(ethnicity = excludedEthnicity, race1 = excludedRace))
25+
val excluded = filterMinorityStatus(source(excludedLars), WhiteNonHispanic)
26+
count(excluded).map(_ mustBe 0)
27+
}
28+
}
29+
30+
"'Other, Including Hispanic' ethnicity filter" must {
31+
"include applications that meet 'Other, Including Hispanic' criteria" in {
32+
def appRace1 = Gen.oneOf(1, 2, 3, 4).sample.get
33+
def appRace2to5 = Gen.oneOf("1", "2", "3", "4", "").sample.get
34+
val lars = larCollectionWithApplicant { app =>
35+
app.copy(ethnicity = 1, race1 = appRace1, race2 = appRace2to5,
36+
race3 = appRace2to5, race4 = appRace2to5, race5 = appRace2to5)
37+
}
38+
39+
val hispanicLars = filterMinorityStatus(source(lars), OtherIncludingHispanic)
40+
count(hispanicLars).map(_ mustBe 100)
41+
}
42+
"exclude applications that do not meet 'Other, Including Hispanic' criteria" in {
43+
def appEthnicity = Gen.oneOf(2, 3, 4).sample.get
44+
val excludedLars = larCollectionWithApplicant(_.copy(ethnicity = appEthnicity, race1 = 5))
45+
val lars = filterMinorityStatus(source(excludedLars), OtherIncludingHispanic)
46+
count(lars).map(_ mustBe 0)
47+
}
48+
}
49+
50+
"ethnicityBorrowerCharacteristic" must {
51+
"generate a MinorityStatusBorrowCharacteristic with both MinorityStatus categories and the specified dispositions" in {
52+
val lars = lar100ListGen.sample.get
53+
val dispositions = List(ReceivedDisp, OriginatedDisp)
54+
55+
val resultF = minorityStatusBorrowerCharacteristic(source(lars), dispositions)
56+
57+
resultF.map { result =>
58+
result mustBe a[MinorityStatusBorrowerCharacteristic]
59+
60+
result.minoritystatus.size mustBe 2
61+
62+
val firstCharacteristic = result.minoritystatus.head
63+
firstCharacteristic mustBe a[MinorityStatusCharacteristic]
64+
firstCharacteristic.minorityStatus mustBe WhiteNonHispanic
65+
firstCharacteristic.dispositions.map(_.disposition) mustBe List(ApplicationReceived, LoansOriginated)
66+
}
67+
}
68+
}
69+
70+
}

0 commit comments

Comments
 (0)