Skip to content

Commit 777309a

Browse files
committed
Tests Using Locust and Faker
1 parent 057e101 commit 777309a

File tree

5 files changed

+306
-0
lines changed

5 files changed

+306
-0
lines changed

pom.xml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@
148148
<version>${scala.plugin.version}</version>
149149
<configuration>
150150
<launchers>
151+
<launcher>
152+
<id>clouseau</id>
153+
<mainClass>com.cloudant.clouseau.Main</mainClass>
154+
<jvmArgs>
155+
<jvmArg>[email protected]</jvmArg>
156+
<jvmArg>-Dclouseau.cookie=monster</jvmArg>
157+
<jvmArg>-Dclouseau.dir=${basedir}/target/clouseau</jvmArg>
158+
</jvmArgs>
159+
</launcher>
151160
<launcher>
152161
<id>clouseau1</id>
153162
<mainClass>com.cloudant.clouseau.Main</mainClass>
@@ -346,6 +355,26 @@
346355
</execution>
347356
</executions>
348357
</plugin>
358+
<!-- Code Coverage report generation -->
359+
<plugin>
360+
<groupId>org.jacoco</groupId>
361+
<artifactId>jacoco-maven-plugin</artifactId>
362+
<version>0.7.9</version>
363+
<executions>
364+
<execution>
365+
<goals>
366+
<goal>prepare-agent</goal>
367+
</goals>
368+
</execution>
369+
<execution>
370+
<id>generate-code-coverage-report</id>
371+
<phase>test</phase>
372+
<goals>
373+
<goal>report</goal>
374+
</goals>
375+
</execution>
376+
</executions>
377+
</plugin>
349378
</plugins>
350379
<extensions>
351380
<extension>

test/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Locust Test
2+
3+
Test `Clouseau` using [Locust](https://github.com/locustio/locust) and [Faker](https://github.com/joke2k/faker).
4+
5+
## Configuration options
6+
7+
Locust configuration options.
8+
9+
Command line | Description
10+
--- | ---
11+
--headless | Disable the web interface, and start the test
12+
--only-summary | Only print the summary stats
13+
--host | Host to load test
14+
-u | Peak number of concurrent Locust users
15+
-r | Rate to spawn users at (users per second)
16+
-t | Stop after the specified amount of time
17+
--docs-number | The number of generated documents (default: 10)
18+
19+
```
20+
locust -f locustfile.py --headless --only-summary --docs-number 10 -u 1 -r 1 -t 10
21+
```
22+
23+
## Basic Usage
24+
25+
Run `CouchDB` and `Clouseau` in different terminals, and then run the locust test:
26+
27+
```
28+
# Open 4 different terminals and run the command:
29+
./dev/run --admin=adm:pass
30+
mvn scala:run -Dlauncher=clouseau1
31+
mvn scala:run -Dlauncher=clouseau2
32+
mvn scala:run -Dlauncher=clouseau3
33+
```
34+
35+
### Install dependencies:
36+
37+
```
38+
./run install
39+
```
40+
41+
### Run random_tree_generator tests:
42+
43+
```
44+
./run locust
45+
```
46+
47+
### Cleanup
48+
49+
```
50+
./run clean
51+
```

test/data.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import json
2+
from datetime import date
3+
from random import choice
4+
from faker import Faker
5+
6+
7+
def write_to_files(data, filename):
8+
with open(filename, 'w') as outfile:
9+
json.dump(data, outfile)
10+
11+
12+
def gen_data(n=10):
13+
data = []
14+
counter = {}
15+
fake = Faker()
16+
fields = ['married', 'ethnicity', 'gender']
17+
counter['total_rows'] = n
18+
19+
for i in range(n):
20+
data.append({'_id': str(i)})
21+
data[i]['gender'] = choice(['M', 'F'])
22+
data[i]['name'] = fake.name_male() if data[i]['gender'] == 'M' else fake.name_female()
23+
data[i]['date_of_birth'] = fake.iso8601()
24+
data[i]['age'] = date.today().year - int(data[i]['date_of_birth'][:4])
25+
data[i]['married'] = 'False' if data[i]['age'] < 22 else choice(['True', 'False'])
26+
data[i]['ethnicity'] = choice(['White', 'Black', 'Asian', 'Hispanic', 'non-Hispanic'])
27+
data[i]['address'] = {'full_address': fake.address()}
28+
data[i]['address']['city'] = data[i]['address']['full_address'][
29+
data[i]['address']['full_address'].find('\n') + 1: -10]
30+
data[i]['address']['area'] = data[i]['address']['full_address'][-8:-6]
31+
data[i]['address']['zip'] = data[i]['address']['full_address'][-5:]
32+
data[i]['lat'] = float(fake.latitude())
33+
data[i]['long'] = float(fake.longitude())
34+
35+
for field in fields:
36+
if field not in counter:
37+
counter[field] = {}
38+
counter[field].update({data[i][field]: counter[field].get(data[i][field], 0) + 1})
39+
40+
write_to_files(data, 'data.json')
41+
write_to_files(counter, 'analysis.json')

test/locustfile.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import json
2+
import requests
3+
import data
4+
5+
from locust import events, HttpUser, constant, task, SequentialTaskSet, tag
6+
7+
URL = 'http://adm:pass@localhost:15984'
8+
DB = 'http://adm:pass@localhost:15984/demo'
9+
SESSION = requests.session()
10+
11+
12+
def create_database():
13+
if SESSION.get(DB).status_code == 200:
14+
SESSION.delete(DB)
15+
SESSION.put(DB)
16+
17+
18+
def insert_docs():
19+
payload = {"docs": []}
20+
with open('data.json') as json_file:
21+
payload['docs'].extend(json.load(json_file))
22+
SESSION.post(DB + '/_bulk_docs', json=payload, headers={"Content-Type": "application/json"})
23+
24+
25+
def create_indexes():
26+
design_docs = {
27+
"_id": "_design/search",
28+
"indexes": {
29+
"search_index": {
30+
"index": "function(doc) {if(doc.gender) {index(\"gender\", doc.gender, {\"store\": true} );};"
31+
"if(doc.age) {index(\"age\", doc.age, {\"store\": true} );};"
32+
"if(doc.married) {index(\"married\", doc.married, {\"store\": true} );};"
33+
"if(doc.ethnicity) {index(\"ethnicity\", doc.ethnicity, {\"store\": true} );}}"
34+
}
35+
}
36+
}
37+
SESSION.put(f'{DB}/_design/search', data=json.dumps(design_docs))
38+
39+
40+
def get_result(condition, response, func_name):
41+
response.success() if condition else response.failure(func_name + ' FAILED.')
42+
43+
44+
@events.init_command_line_parser.add_listener
45+
def _(parser):
46+
parser.add_argument("--docs-number", type=int, env_var="LOCUST_DOCS_NUMBER", default=5,
47+
help="How many documents do you want to generate")
48+
49+
50+
@events.test_start.add_listener
51+
def _(environment, **kw):
52+
print('1. Generate documents')
53+
data.gen_data(environment.parsed_options.docs_number)
54+
55+
56+
class CouchDBTest(SequentialTaskSet):
57+
def on_start(self):
58+
self.client.get('/', name=self.on_start.__name__)
59+
print('2. Create Database, Insert docs and _design docs')
60+
create_database()
61+
insert_docs()
62+
create_indexes()
63+
print('3. Start testing ... ')
64+
with open('analysis.json') as json_file:
65+
self.data = json.load(json_file)
66+
67+
@tag('get')
68+
@task
69+
def get_all_dbs(self):
70+
with self.client.get('/demo/_all_dbs', catch_response=True, name='Get All DBs') as response:
71+
get_result(
72+
len(response.text) and response.elapsed.total_seconds() < 2.0,
73+
response, self.get_all_dbs.__name__)
74+
75+
@tag('get')
76+
@task
77+
def get_all_docs(self):
78+
with self.client.get('/demo/_all_docs', catch_response=True, name='Get All Docs') as response:
79+
get_result(
80+
len(response.text) and response.elapsed.total_seconds() < 2.0,
81+
response, self.get_all_docs.__name__)
82+
83+
@tag('search')
84+
@task
85+
def search_all_docs(self):
86+
with self.client.get('/demo/_design/search/_search/search_index?query=*:*',
87+
catch_response=True, name='Search All Docs') as response:
88+
get_result(
89+
response.status_code == 200 and response.json()['total_rows'] == self.data['total_rows'],
90+
response, self.search_all_docs.__name__)
91+
92+
@tag('search')
93+
@task
94+
def search_gender_is_male(self):
95+
with self.client.get('/demo/_design/search/_search/search_index?query=gender:m',
96+
catch_response=True, name='Search Gender is Male') as response:
97+
get_result(
98+
response.status_code == 200 and response.json()['total_rows'] == self.data['gender']['M'],
99+
response, self.search_gender_is_male.__name__)
100+
101+
@tag('search')
102+
@task
103+
def search_gender_is_male_with_limit_2(self):
104+
with self.client.get('/demo/_design/search/_search/search_index?query=gender:m&limit=2',
105+
catch_response=True, name='Search Gender is Male with Limit 2') as response:
106+
get_result(
107+
response.status_code == 200 and len(response.json()['rows']) == 2,
108+
response, self.search_gender_is_male_with_limit_2.__name__)
109+
110+
@tag('search')
111+
@task
112+
def search_gender_is_female_and_sort_by_age(self):
113+
with self.client.get('/demo/_design/search/_search/search_index?query=gender:f&sort="age"',
114+
catch_response=True, name='Search Gender is Female AND Sort by age') as response:
115+
result = response.json()
116+
if self.data['gender']['F'] >= 2:
117+
conditions = result['total_rows'] == self.data['gender']['F'] and \
118+
result['rows'][0]['fields']['age'] <= result['rows'][1]['fields']['age']
119+
else:
120+
conditions = result['total_rows'] == self.data['gender']['F']
121+
get_result(conditions, response, self.search_gender_is_female_and_sort_by_age.__name__)
122+
123+
@tag('search')
124+
@task
125+
def search_married_people_age_should_greater_than_21(self):
126+
with self.client.get(
127+
'/demo/_design/search/_search/search_index?query=married:true',
128+
catch_response=True, name='Search married people age > 21') as response:
129+
result = response.json()
130+
for i in result['rows']:
131+
if i['fields']['age'] <= 21:
132+
response.failure(self.search_married_people_age_should_greater_than_21.__name__)
133+
response.success()
134+
135+
@tag('search')
136+
@task
137+
def search_ethnicity_White_OR_Asian(self):
138+
with self.client.get(
139+
f'/demo/_design/search/_search/search_index?query=ethnicity:White OR ethnicity:Asian',
140+
catch_response=True, name='Search ethnicity White OR Asian') as response:
141+
result = response.json()
142+
get_result(
143+
response.status_code == 200 and
144+
result['total_rows'] == self.data['ethnicity']['White'] + self.data['ethnicity']['Asian'],
145+
response, self.search_ethnicity_White_OR_Asian.__name__)
146+
147+
def on_stop(self):
148+
self.client.get('/', name=self.on_stop.__name__)
149+
print("4. Delete database, and shut down the locust")
150+
SESSION.delete(DB)
151+
152+
153+
class LoadTest(HttpUser):
154+
host = URL
155+
wait_time = constant(1)
156+
tasks = [CouchDBTest]

test/run

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
case $1 in
3+
h | help)
4+
echo "Common options:
5+
h, help Show this help message
6+
i, install Install dependencies
7+
l, locust Run locust tests
8+
(Specify the docs number to be tested, run $./run l {docs_number}})
9+
c, cleanup Cleanup the directory"
10+
;;
11+
i | install)
12+
echo Install dependencies
13+
python3 -m pip install Faker locust
14+
;;
15+
l | locust)
16+
if [ $2 -ge 1 ]; then
17+
locust -f locustfile.py --headless --only-summary --docs-number $2 -u 1 -r 1 -t 600
18+
else
19+
locust -f locustfile.py --headless --only-summary --docs-number 10 -u 1 -r 1 -t 10
20+
fi
21+
;;
22+
c | clean)
23+
rm -rf analysis.json data.json
24+
echo cleanup DONE
25+
;;
26+
*)
27+
echo don\'t know
28+
;;
29+
esac

0 commit comments

Comments
 (0)