Bingo for meetings- Adding a CLI application–part 12
Bingo
Bingo is a small project, written in TypeScript , and developed with Alexandru Badita in launch break (one hour - more or less). You can find sources at https://github.com/alexandru360/PresentationBingoCards/ . Those are my blog posts for Bingo : ( scroll below for the post)The easy way to test an application is to make a simple command line version . You can find the code at https://github.com/alexandru360/PresentationBingoCards/blob/master/bingo-meeting-console/index.ts
You can run also with Docker by running https://github.com/alexandru360/PresentationBingoCards/blob/master/dockerize/bingo_cli.bat
What were the steps ?
1. Install the @types/node, ts-node , typescript and others – see
2. Create an index.ts with the required CLI ( hint: prompt, figlet, chalk , username , inquirer,console.table are good to have) – read https://dev.to/hugodias/building-your-first-interactive-node-js-cli-1g2c
3. Created an async main() and calling with
(async () => { try { await main(); } catch (e) { console.log(JSON.stringify(e)); } })();
4. Modify package.json to have
“main”: “dist/index.js”,
“types”: “dist/index.d.ts”,
“scripts”: {
“start”: “ts-node index.ts”,
“build”: “tsc”,
“compile”: “tsc”
}
5. Put in the root package
“scripts”: {
“test”: “cd bingo-meeting-objects-test && yarn test”,
“runConsole”: “cd bingo-meeting-console && yarn start”
}
and then run yarn runConsole
I can say that the C# console experience is better 😉
You can download the source code from https://github.com/alexandru360/PresentationBingoCards/releases/tag/CLI
Bingo for meetings- yarn workspaces–part 11
Bingo
Bingo is a small project, written in TypeScript , and developed with Alexandru Badita in launch break (one hour - more or less). You can find sources at https://github.com/alexandru360/PresentationBingoCards/ . Those are my blog posts for Bingo : ( scroll below for the post)The problem that we see is how to have the same objects configured for backend and for frontend( e.g. a Meeting is used on the backend on the WebAPI to read from database and in the frontend to display)
In C# , there is the concept of dll / assembly that is common. In here we have the concept of yarn Workspaces : https://yarnpkg.com/lang/en/docs/cli/workspace/
What we want to achieve first is that the tests will be in a separate workspace bingo-meeting-objects-test , referencing the bingo-meeting-objects workspace
So, in order to do this, we have to modify :
- yarn workspaces to understand the structure and run tests
- bingo-meeting-objects to expose the result
- bingo-meeting-objects–test to import bingo-meeting-objects
- (depending on the projects) Other references : Modify docker bat file to consider the new structure
Let’s detail:
For yarn workspaces to understand the structure and run tests
So , first, we move the tests in a separate folder, bingo-meeting-objects–test , and we run nom init and add dependencies( jest, others)
Second, we add the yarn workspace package.json in the root with the following content:
{
“private”: true,
“workspaces”: [“bingo-cards-api”, “bingo-meeting-objects”]
}
“workspaces”: [“bingo-cards-api”, “bingo-meeting-objects”, “bingo-meeting-objects-test”],
“scripts”: {
“test”: “cd bingo-meeting-objects-test && yarn test”
}
}
In this manner, we can run yarn test from the root ( do not run yet!)
For bingo-meeting-objects to expose the result
In the tsconfig.json we put those lines:
“sourceMap”: true,
“declaration”: true,
In the package.json we modify to understand the declaration
“main”: “dist/index.js”,
“types”: “dist/index.d.ts”,“scripts”: {
“build”: “tsc”,
“compile”: “tsc”,
“test”: “jest”
},
For bingo-meeting-objects–test to import bingo-meeting-objects
We add jest and others. Also, we added dependency of bingo-meeting-objects:
“dependencies”: {
“bingo-meeting-objects”: “^1.0.0”,
Also, we need to modify the import of the test. Instead of the following line, when test was under subfolder test in the bingo-meeting-objects folder
import MeetingsFactory from ‘../MeetingsFactory’;
we put
import MeetingsFactory from “bingo-meeting-objects/MeetingsFactory”;
Now running yarn test in the root folder runs the test sucessfully
For (depending on the projects) Other references : Modify docker bat file to consider the new structure
We had the batch file that uses docker that was running the tests and then copy the results.
For start, we move the .dockerignore from the project to the root ( to not put node_modules)
Also, we modify how we copy the files to docker
For this, we modify this line
docker cp bingo_ci_test_container:/app/coverage/cobertura-coverage.xml .
into this line
docker cp bingo_ci_test_container:/app/bingo-meeting-objects-test/coverage/cobertura-coverage.xml .
to take into consideration the new structure
And that will be all! ( code source at https://github.com/alexandru360/PresentationBingoCards/releases/tag/yarnworkspaces )
Bingo for meetings- azure integrations–part 10
Bingo
Bingo is a small project, written in TypeScript , and developed with Alexandru Badita in launch break (one hour - more or less). You can find sources at https://github.com/alexandru360/PresentationBingoCards/ . Those are my blog posts for Bingo : ( scroll below for the post)Now it is about Continuous Integrations. We want tests to be run each time we push something to GitHub. For this we could use Azure DevOps. It is free for GitHub public repositories . We want to configure an azure pipeline to automatically run tests that we have in Docker.
So the pipeline will just have to explicit gather the test results ( tests + code coverage ) in order to display in the Azure Pipeline and in the project. Azure DevOps wants the test coverage in JaCoCo or Cobertura . Jest has Istanbul as default test coverage, and Istanbul has Cobertura report. So we modify the jest.config.js to support cobertura
module.exports = {
preset: ‘ts-jest’,
transform: {
‘^.+\\.tsx?$’: ‘ts-jest’,
},
testEnvironment: ‘node’,
collectCoverage: true,
coverageReporters : [“json”, “lcov”, “text”, “clover”,”cobertura”]
};
And to copy when docker building the tests to the local path
docker build ../Src -f docker_ci_test.txt -t bingo_ci_test
docker run -d –rm –name bingo_ci_test_container bingo_ci_test
docker cp bingo_ci_test_container:/app/jest-stare .
docker cp bingo_ci_test_container:/app/junit.xml .
docker cp bingo_ci_test_container:/app/coverage/cobertura-coverage.xml .
docker container kill bingo_ci_test_container
And then copy to the AzureDevOps test system
#https://docs.microsoft.com/en-us/azure/devops/pipelines/build/options?view=vsts&tabs=yaml
variables:
year: $(Date:yyyy)
month: $(Date:MM)
day: $(Date:dd)
uk: $(Date:yyyyMMdd)
messagePush: $(Build.SourceVersionMessage)
name: $(TeamProject)_$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
jobs:
– job: FullTestOnLinux
pool:
vmImage: ‘ubuntu-16.04’
steps:
– checkout: self #skip checking out the default repository resource
clean: true
– script: |
cd dockerize
ls -l
chmod 777 ./ci_test.bat
./ci_test.bat
docker image ls
docker container ls
cp -r -v ./jest-stare $(Build.ArtifactStagingDirectory)/jest-stare/
cp ./junit.xml $(Build.ArtifactStagingDirectory)/junit.xml
cp ./cobertura-coverage.xml $(Build.ArtifactStagingDirectory)/cobertura-coverage.xml
displayName: test DDD
– task: PublishBuildArtifacts@1
inputs:
artifactName: Tests
displayName: ‘Publish Artifact: drop’
– task: PublishTestResults@2
inputs:
testRunner: JUnit
testResultsFiles: ‘$(Build.ArtifactStagingDirectory)/junit.xml’
– task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: ‘cobertura’
summaryFileLocation: ‘$(Build.ArtifactStagingDirectory)/cobertura-coverage.xml’
You can see the tests and the test coverage at https://dev.azure.com/ignatandrei0674/BingoAzureDevOps/_build/results?buildId=953&view=ms.vss-test-web.build-test-results-tab
Bingo for meetings- dockerize tests–part 9
Bingo
Bingo is a small project, written in TypeScript , and developed with Alexandru Badita in launch break (one hour - more or less). You can find sources at https://github.com/alexandru360/PresentationBingoCards/ . Those are my blog posts for Bingo : ( scroll below for the post)We have now full DDD and tests that should be run for the objects. However, we need a way to automatically have the tests run . The
easy way is to dockerize the tests – run in a container, grab the results, display somewhere.
First we should have the tests display in a nice form some data. For this, jest have the “reporters” features – but no documentation . So I try to find and https://github.com/dkelosky/jest-stare .
So what are the steps ?
- Create docker from node
- Copy sources ( add a .dockerignore to not copy node_modules)
- Install dependencies
- Run test
- run image into container and grab the tests results
The docker file , named docker_ci_test.txt , has the following content
FROM node:8
WORKDIR /app
COPY . ./
RUN yarn
RUN yarn test –reporters default jest-stare
CMD tail -f /dev/null
The bat that runs the image and grab results from the container
docker build ../src -f docker_ci_test.txt -t bingo_ci_test
docker run -d –rm –name bingo_ci_test_container bingo_ci_test
docker cp bingo_ci_test_container:/app/jest-stare .
docker container kill bingo_ci_test_container
Feel free to download the project from https://github.com/alexandru360/PresentationBingoCards/ and run the ci_test.bat file from dockerize folder.
Friday Links 331
Bingo for meetings–hide results–part 8
Bingo
Bingo is a small project, written in TypeScript , and developed with Alexandru Badita in launch break (one hour - more or less). You can find sources at https://github.com/alexandru360/PresentationBingoCards/ . Those are my blog posts for Bingo : ( scroll below for the post)The last requirement says
EndMeeting:
The score of how many cards/ what cards were checked will be available 1 hour and 5 minutes
We have already done something similar when the meeting was considered obsolete – we decided to not throw error, but use
Result< T, Error> . So I supposed to be easy …. However, it was not. Because we are modifying the results of the functions, we are supposed to modify also the tests….
First , then modification of the code. Because of the refactoring, the AllUnchecked function calls TotalNumberOfCardsChecked.
public TotalNumberOfCardsChecked():number{ return this.Cards.filter(it=>it.IsChecked()).length ; } public AllUnchecked(): boolean{ return (this.TotalNumberOfCardsChecked() === 0); }
Now, because we cannot TotalNumberOfCardsChecked returns Result<number, Error> , the function AllUnchecked must be re-written :
public TotalNumberOfCardsChecked():Result<number, Error>{ if(this.CanSeeScore()){ return ok(this.Cards.filter(it=>it.IsChecked()).length ); } else{ //TODO :Make a proper error return err(new Error(`cannot see score for ${this.Id}`)); } } public AllUnchecked(): Result< boolean, Error>{ var res=this.TotalNumberOfCardsChecked(); console.log(" all unchecked" + res.isOk()); return res.andThen(it=> ok(it===0)); // const ret= res.match( // (v)=>{return ok(v==0)}, // (error)=>{ return error} // ); // return ret; }
Second , the test will fail. Take “card should be checked” – original code was pretty easy
import MeetingsFactory from '../MeetingsFactory'; import Meeting from '../meeting'; describe('Check card basic', () => { it('card should be checked', () => { const mf=new MeetingsFactory(); const m1=mf.CreateMeeting("andrei","first meeting"); console.log(m1.Cards.length); expect(m1.AllUnchecked()).toBe(true); m1.CheckCard(m1.Cards[0], m1.Participants[0]); expect(m1.AllUnchecked()).toBe(false); expect(m1.IsCardCheckedByParticipant(m1.Cards[0], m1.Participants[0])).toBe(true); }) })
Now it is
it('card should be checked', () => { const mf=new MeetingsFactory(); const m1=mf.CreateMeeting("andrei","first meeting"); console.log(m1.Cards.length); let res=m1.AllUnchecked(); expect(res.isOk()).toBe(true); let result= false; res.map(t=> result =t); expect(result).toBe(true); let res1 = m1.CheckCardByParticipant(m1.Cards[0], m1.Participants[0]); expect(res1.isOk()).toBe(true); res=m1.AllUnchecked(); expect(res.isOk()).toBe(true); result= false; res.map(t=> result =t); expect(result).toBe(false); expect(m1.IsCardCheckedByParticipant(m1.Cards[0], m1.Participants[0]).isOk()).toBe(true); })
Bingo for meetings–obsolete–re-reading requirements- part 7
Bingo
Bingo is a small project, written in TypeScript , and developed with Alexandru Badita in launch break (one hour - more or less). You can find sources at https://github.com/alexandru360/PresentationBingoCards/ . Those are my blog posts for Bingo : ( scroll below for the post)Last time we have to implement the requirement:
Meeting Obsolete:
The meeting is available for 35 minutes. After that, meeting is not available anymore.
We did so for the 35 minutes. But we did not for the last sentence – meeting is not available anymore. There are 2 points here: of design ( do not retrieve meeting ) and enforcing that no participant can write to the meeting.
So we modify the functions AddParticipant, CheckCardByParticipant to raise an exception if the meeting is obsolete .( As a side effect of identifying the functions that make actions in the opposite of functions that just reports, I think about structuring code in CQRS form )
Now for TypeScript we have 2 options :
- Construct a class that inherits from Error
- Modify the response type of those function to a combined type of result and error
For the first one , we should take care of TypeScript syntax of creating errors: See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html and https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
class CustomError extends Error {
constructor(message?: string) {
super(message);
// ‘Error’ breaks prototype chain here
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
}
}
For the second , we could do ourselves or use a npm package such as https://github.com/gDelgado14/neverthrow
I decide to go to the second. I have too many times use the first thing in C# – it is time to have something new. Anyway, this is the code now
import { ok, err, Result } from 'neverthrow'; //code public CheckCardByParticipant(c: Cards , p:Participant): Result<Meeting,Error>{ //TODO: verify participant is added first or add //TODO: verify card is added first if(this.IsObsolete()){ return err(new Error(`cannot check card to the obsolete meeting ${this.Id}`)); } c.CheckMe(p); return ok(this); } //code public AddParticipant(p:Participant ): Result<number,Error>{ if(this.IsObsolete()){ return err(new Error(`cannot add participant to the obsolete meeting ${this.Id}`)); } this.Participants.push(p); return ok(this.Participants.length); }
and those are the tests
const mf=new MeetingsFactory(); const m1=mf.CreateMeeting("andrei","first meeting"); const now = Date.now(); const spy = jest.spyOn(Date,'now'); spy.mockImplementation(()=>{ console.log('calling DateTime Now'); return now + 36 * 60* 1000; } ); expect(m1.IsObsolete()).toBe(true); const p=new Participant(); p.Id=70; p.Name ="alexandru"; const res= m1.AddParticipant(p); expect(res.isOk()).toBe(false); spy.mockRestore();
Bingo for meetings–obsolete–part 6
Bingo
Bingo is a small project, written in TypeScript , and developed with Alexandru Badita in launch break (one hour - more or less). You can find sources at https://github.com/alexandru360/PresentationBingoCards/ . Those are my blog posts for Bingo : ( scroll below for the post)The requirement says:
Meeting Obsolete:
The meeting is available for 35 minutes. After that, meeting is not available anymore.
How we can implement this ? Several solutions:
- Make the meeting know about this ( and avoiding https://martinfowler.com/bliki/AnemicDomainModel.html )
- Make a decorator class for this https://en.wikipedia.org/wiki/Decorator_pattern
- Make a mixin in TypeScript https://www.typescriptlang.org/docs/handbook/mixins.html
I decide to KISS (https://en.wikipedia.org/wiki/KISS_principle ) and take the first point. We implement an Obsolete function. Remains who is responsible of the site to call it.
Now comes other question: When the meeting starts ?I consider ( for the sake of easy path) that a meeting starts when it is created . So the code is like this
export default class Meeting{ constructor(){ this.Participants = []; this.Cards = []; this.startedMeeting = Date.now(); } public static MaxTime=35 * 60 * 1000; // other code here public IsObsolete(): boolean{ return (this.PassedTimeFromStart() > Meeting.MaxTime); //35 minutes } public PassedTimeFromStart():number{ let dtNow = Date.now(); console.log(dtNow); return (dtNow - this.startedMeeting ); }
Now the problem arises when testing code. It is very easy to say that a meeting is not obsolete when just created. However , I do not want to wait for 35minutes + 1 second in order for a meeting to become obsolete and the test to be successful( see
//TODO: wait 35 minutes + 1 second
below).
import MeetingsFactory from '../MeetingsFactory'; import Meeting from '../meeting'; describe('Meeting Obsolete', () => { it('meeting should not be obsolete after creation', () => { const mf=new MeetingsFactory(); const m1=mf.CreateMeeting("andrei","first meeting"); expect(m1.IsObsolete()).toBe(false); }) it('meeting should be obsolete after 35 minutes', () => { const mf=new MeetingsFactory(); const m1=mf.CreateMeeting("andrei","first meeting"); //TODO: wait 35 minutes + 1 second expect(m1.IsObsolete()).toBe(true); }) })
One possible resolution is https://ayende.com/blog/3408/dealing-with-time-in-tests . However, the problem is so common that here must be include in the test framework ( in this case, jest). And , indeed, it is a way: spyon. So the code is modified accordingly :
it('meeting should be obsolete after 35 minutes', () => { const mf=new MeetingsFactory(); const m1=mf.CreateMeeting("andrei","first meeting"); const now = Date.now(); const spy = jest.spyOn(Date,'now'); spy.mockImplementation(()=>{ console.log('calling DateTime Now'); return now + 36 * 60* 1000; } ); expect(m1.IsObsolete()).toBe(true); //spy.mockClear(); spy.mockRestore(); expect(m1.IsObsolete()).toBe(false); })
The testing code now assumes that he knows inner working of the code – and the test will fail if we modify the call of Date.now().
So I think that https://ayende.com/blog/3408/dealing-with-time-in-tests is far superior.
Bingo for meetings–working at score-part 5
Bingo
Bingo is a small project, written in TypeScript , and developed with Alexandru Badita in launch break (one hour - more or less). You can find sources at https://github.com/alexandru360/PresentationBingoCards/ . Those are my blog posts for Bingo : ( scroll below for the post)The next requirement is saying ( https://github.com/alexandru360/PresentationBingoCards/projects/1#card-24165817 )
Checking cards:
A total score will be displayed when checking / unchecking
There are 2 things very clear:
- The name of the user story is not reflecting the content
- It should not be just the total score, but also should be the percentage of the bingo cards checked from the total number of cards. It is not so difficult to have the total number of cards, so let’s add this.
We have put this code:
public TotalNumberOfCardsChecked():number{ return this.Cards.filter(it=>it.IsChecked()).length ; }
And we put test
it('number of cards checked', () => { const mf=new MeetingsFactory(); const m1=mf.CreateMeeting("andrei","first meeting"); console.log(m1.Cards.length); expect(m1.TotalNumberOfCardsChecked()).toBe(0); m1.CheckCardByParticipant(m1.Cards[0], m1.Participants[0]); expect(m1.TotalNumberOfCardsChecked()).toBe(1); })
We see that for TotalNumberOfCardsChecked and AllUnchecked we have the same code
public AllUnchecked(): boolean{ return (this.Cards.filter(it=>it.IsChecked()).length === 0); } //other code public TotalNumberOfCardsChecked():number{ return this.Cards.filter(it=>it.IsChecked()).length ; }
and because we hate copy paste we refactor and re-test
So we refactor a bit .
public AllUnchecked(): boolean{ return (this.TotalNumberOfCardsChecked() === 0); }
And because we have tests, that means we are pretty confident of what are we doing
Also, we said that we have to calculate the total number of checked cards. This is not so difficult, and it alleviates the Law of Dots /Demeter (https://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx)
public TotalNumberOfCards():number{ return this.Cards.length; }
The consequence ? To maintain code coverage, we should add another line of test:
expect(m1.TotalNumberOfCards()).toBe(Cards.DefaultCards().length);
Also, because the requirement says to display, and we are not yet to the GUI, I have put a new note that says
Design:
– [ ] display total number of cards checked or percentage ( use TotalNumberOfCardsChecked and/or TotalNumberOfCards)