SalesforceのApexでチームスピリットの出退勤情報からGoogleカレンダーに登録する処理

GoogleカレンダーにSalesforce側からデータを渡すにはどうするのがいいかなと思いながら数年かかってしまったのですが、時間ができたので書いてみました。とりあえずここまでできれば、あとはいくらでも応用がききそうです。

表題の通りなのですが、やることが多いので順番に書いてみます。
やることは大きく分けて2つ。
- SalesforceからGoogleにアクセスする設定をすること。
- Apexでコード書く

## SalesforceからGoogleにアクセスする設定をする
https://upward.jp/weblog/20150316namedcredential/
こちらを参照の上、設定を行いましょう。
画像がテキストと前後しているので若干混乱するかもしれませんが概ね大丈夫です。
画像ではユーザ情報エンドポイントURLにURL入っているのですが、ここに文字が入っているとエラーがでるので注意

## Apexでコード書く
今回はトリガで書きました。バッチ処理で書くパターンもありそうな気がします。

## なにやってるのか
- チームスピリットの出勤時間と退勤時間が揃ったとき
- ユーザーがGoogleカレンダーへの同期を希望しているとき User.IsTsSyncToGoogleCalendar__c == TRUE

上記の2条件が揃ったときに、出退勤時間をGoogleカレンダーに登録しています。
カレンダーには、サブジェクトが「勤怠日時」 descriptionが「Teamspiritに登録された情報です」　という文字で入りますが、こちらハードコーディングされているので適当に変更してもよいでしょう。

トリガはシンプルです。ハンドラに渡します

``` GCalTrriger.apxt
trigger GCalTrriger on teamspirit__AtkEmpDay__c (after insert,after update) {
    editGoogleCalendarEvent handler = new editGoogleCalendarEvent();
    if (Trigger.isInsert) {
	    handler.onAfterInsert(Trigger.new);
    }
    if (Trigger.isUpdate) {
	    handler.onAfterUpdate(Trigger.old,Trigger.new);
    }
}
```
実行クラスにはカスタム項目がいくつか設定してあります。
勤怠日次
- IsTsSyncToGoogleCalendar__c 勤怠日次＞勤怠月次＞ユーザのIsTsSyncToGoogleCalendar__cを参照しています。ユーザにはチェックボックス項目として存在しており、こちらがTRUEになっているユーザのみ処理を行います。
- Google_Calendar_eventId__c テキスト項目で、GoogleカレンダーのイベントIDを格納します。これを入れておくことで、修正や削除に対応しています。

reqPath = 'callout:GoogleCalendar/calendars/' + userEmail + '/events/' + gcalid;

という行については[GoogleCalendar]がどこから来ているかというと指定ログイン情報をSalesforceで設定したときの名前です。


``` editGoogleCalendarEvent.apxc
public class editGoogleCalendarEvent {
    public void onAfterUpdate(LIST<teamspirit__AtkEmpDay__c> tsEmpDaysOld,LIST<teamspirit__AtkEmpDay__c> tsEmpDaysNew) {
        Integer listSize = tsEmpDaysOld.size();
        LIST<teamspirit__AtkEmpDay__c>tsEmpDaysHasData = new LIST<teamspirit__AtkEmpDay__c>();
        LIST<teamspirit__AtkEmpDay__c>tsEmpDaysDelete = new LIST<teamspirit__AtkEmpDay__c>();
        for(Integer i = 0; i < listSize;i++){
            if(tsEmpDaysNew[i].IsTsSyncToGoogleCalendar__c ==True){//ユーザがカレンダー連携したい場合
                system.debug('tsEmpDaysNew[i].teamspirit__StartTime__c ' + tsEmpDaysNew[i].teamspirit__StartTime__c + ' tsEmpDaysNew[i].teamspirit__EndTime__c: ' + tsEmpDaysNew[i].teamspirit__EndTime__c);
                //始業時間・退勤時間が入っている場合
                if(tsEmpDaysNew[i].teamspirit__StartTime__c != NULL && tsEmpDaysNew[i].teamspirit__EndTime__c != NULL){//始業時間・退勤時間が入っている場合
                    if(tsEmpDaysNew[i].Google_Calendar_eventId__c == NULL || tsEmpDaysNew[i].Google_Calendar_eventId__c == ''){//Googleに入ってない場合は処理する
                        tsEmpDaysHasData.add(tsEmpDaysNew[i]);
                    }else if( ((tsEmpDaysOld[i].teamspirit__StartTime__c != tsEmpDaysNew[i].teamspirit__StartTime__c) ||   (tsEmpDaysOld[i].teamspirit__EndTime__c != tsEmpDaysNew[i].teamspirit__EndTime__c))){
                        //始業時間or退勤時間が変更された場合は処理する
                        tsEmpDaysHasData.add(tsEmpDaysNew[i]);                
                    }
                }else if(tsEmpDaysNew[i].teamspirit__StartTime__c == NULL && tsEmpDaysNew[i].teamspirit__EndTime__c == NULL && ( tsEmpDaysNew[i].Google_Calendar_eventId__c != NULL && tsEmpDaysNew[i].Google_Calendar_eventId__c != '')){//始業時間・退勤時間が両方入っていない場合
                    tsEmpDaysDelete.add(tsEmpDaysNew[i]);
                }
            }
        }
        if(tsEmpDaysHasData.size()>0){
            // teamspirit__StartTime__cとteamspirit__EndTime__c　は hh * 60 + mm という形式の数値になってる
            // mim = math.mod(Integer.valueOf(teamspirit__StartTime__c),60)
            // hour = Integer.valueOf(teamspirit__StartTime__c)/60
            for(teamspirit__AtkEmpDay__c data : tsEmpDaysHasData){
                String startString = String.valueOf(data.teamspirit__Date__c.year())+ '-' + String.valueOf(data.teamspirit__Date__c.month()).leftPad(2, '0')+ '-' + String.valueOf(data.teamspirit__Date__c.day()).leftPad(2, '0')+ 'T' + String.valueOf(Integer.valueOf(data.teamspirit__StartTime__c)/60).leftPad(2, '0')+ ':'+String.valueOf(math.mod(Integer.valueOf(data.teamspirit__StartTime__c),60)).leftPad(2, '0')+':00+09:00';
                String endString = String.valueOf(data.teamspirit__Date__c.year())+ '-' + String.valueOf(data.teamspirit__Date__c.month()).leftPad(2, '0')+ '-' + String.valueOf(data.teamspirit__Date__c.day()).leftPad(2, '0')+ 'T' + String.valueOf(Integer.valueOf(data.teamspirit__EndTime__c)/60).leftPad(2, '0')+ ':'+String.valueOf(math.mod(Integer.valueOf(data.teamspirit__EndTime__c),60)).leftPad(2, '0')+':00+09:00';
                String userEmail = data.userEmail__c;
                String AtkEmpDayID = data.id;
                String gcalid = '';
                if(data.Google_Calendar_eventId__c!=NULL){
                    gcalid = data.Google_Calendar_eventId__c;
                }
                system.debug('hello');
                sendToGoogle(userEmail,startString,endString,AtkEmpDayID,gcalid);
            }
        }
        if(tsEmpDaysDelete.size()>0){
            for(teamspirit__AtkEmpDay__c data : tsEmpDaysDelete){
                String userEmail = data.userEmail__c;
                String AtkEmpDayID = data.id;
                String gcalid = data.Google_Calendar_eventId__c;
                sendToGoogleToDelete(userEmail,AtkEmpDayID,gcalid);
            }
            
        }
    }

    public void onAfterInsert(LIST<teamspirit__AtkEmpDay__c> tsEmpDaysNew) {
        LIST<teamspirit__AtkEmpDay__c>tsEmpDaysHasData = new LIST<teamspirit__AtkEmpDay__c>();
        for(teamspirit__AtkEmpDay__c ed : tsEmpDaysNew){
            //始業時間・退勤時間が入っている場合 且つ ユーザがカレンダー連携したい場合のみ処理する
            if( ed.teamspirit__StartTime__c != NULL && ed.teamspirit__EndTime__c != NULL && ed.IsTsSyncToGoogleCalendar__c == TRUE){
                tsEmpDaysHasData.add(ed);                
            }
        }
        if(tsEmpDaysHasData.size()>0){
            // teamspirit__StartTime__cとteamspirit__EndTime__c　は hh * 60 + mm という形式の数値になってる
            // mim = math.mod(Integer.valueOf(teamspirit__StartTime__c),60)
            // hour = Integer.valueOf(teamspirit__StartTime__c)/60
            for(teamspirit__AtkEmpDay__c data : tsEmpDaysHasData){
                String startString = String.valueOf(data.teamspirit__Date__c.year())+ '-' + String.valueOf(data.teamspirit__Date__c.month()).leftPad(2, '0')+ '-' + String.valueOf(data.teamspirit__Date__c.day()).leftPad(2, '0')+ 'T' + String.valueOf(Integer.valueOf(data.teamspirit__StartTime__c)/60).leftPad(2, '0')+ ':'+String.valueOf(math.mod(Integer.valueOf(data.teamspirit__StartTime__c),60)).leftPad(2, '0')+':00+09:00';
                String endString = String.valueOf(data.teamspirit__Date__c.year())+ '-' + String.valueOf(data.teamspirit__Date__c.month()).leftPad(2, '0')+ '-' + String.valueOf(data.teamspirit__Date__c.day()).leftPad(2, '0')+ 'T' + String.valueOf(Integer.valueOf(data.teamspirit__EndTime__c)/60).leftPad(2, '0')+ ':'+String.valueOf(math.mod(Integer.valueOf(data.teamspirit__EndTime__c),60)).leftPad(2, '0')+':00+09:00';
                String userEmail = data.userEmail__c;
                String AtkEmpDayID = data.id;
                String gcalid = '';
                sendToGoogle(userEmail,startString,endString,AtkEmpDayID,gcalid);
            }
        }
    }    
    
    @Future(callout=true)
    public static void sendToGoogle(String userEmail,String startString,String endString,String AtkEmpDayID,String gcalid) {
        Http http = new Http();
        HttpRequest req = new HttpRequest();

        // 指定ログイン情報を使用
        String reqPath;
        if(gcalid == ''){
	        req.setMethod('POST'); 
	        reqPath = 'callout:GoogleCalendar/calendars/' + userEmail + '/events';
        }else{
	        req.setMethod('PUT');                    
	        reqPath = 'callout:GoogleCalendar/calendars/' + userEmail + '/events/' + gcalid;
        }
        req.setEndpoint(reqPath);
        req.setHeader('content-type', 'application/json');
        req.setBody('{ "end": {  "dateTime": "' + endString + '" }, "start": {  "dateTime": "' + startString + '" }, "description": "Teamspiritに登録された情報です", "summary": "勤怠日時"}');
		/*
		 * 	{ "end": {  "dateTime": 
		 */
        System.debug('req：' + req);
        System.debug('req.setBody ' + '{ "end": {  "dateTime": "' + endString + '" }, "start": {  "dateTime": "' + startString + '" }, "description": "Teamspiritに登録された情報です", "summary": "勤怠日時"}');
        HttpResponse res = http.send(req);
        System.debug('Res：' + res);
        System.debug('res body ' + res.getBody());
        GoogleCalResponseBodyJSON2Apex obj = GoogleCalResponseBodyJSON2Apex.parse(res.getBody());
        system.debug('obj : ' + obj);
        String objid = obj.id;
        system.debug('objid ' + objid);
        teamspirit__AtkEmpDay__c updateAtkEmpDay = new teamspirit__AtkEmpDay__c(id = AtkEmpDayID,Google_Calendar_eventId__c = objid);
        system.debug('updateAtkEmpDay ' + updateAtkEmpDay);
        update updateAtkEmpDay;
    }
    
    @Future(callout=true)
    public static void sendToGoogleToDelete(String userEmail,String AtkEmpDayID,String gcalid) {
        Http http = new Http();
        HttpRequest req = new HttpRequest();

        // 指定ログイン情報を使用
        String reqPath;
        req.setMethod('DELETE');                    
        reqPath = 'callout:GoogleCalendar/calendars/' + userEmail + '/events/' + gcalid;
        req.setEndpoint(reqPath);
        req.setHeader('content-type', 'application/json');
        HttpResponse res = http.send(req);
        System.debug('Res：' + res);
        System.debug('res body ' + res.getBody());
        teamspirit__AtkEmpDay__c updateAtkEmpDay = new teamspirit__AtkEmpDay__c(id = AtkEmpDayID,Google_Calendar_eventId__c = '');
        system.debug('updateAtkEmpDay ' + updateAtkEmpDay);
        update updateAtkEmpDay;
    }
}
```

Googleカレンダーに投げたあとに返ってくるレスポンスはJSONなのですが、これはApexではパースできませんのでパーサーを作ります。
多分皆さん同じコードで使えると思う。

``` GoogleCalResponseBodyJSON2Apex
//
// Generated by JSON2Apex http://json2apex.herokuapp.com/
//
// The supplied json has fields with names that are not valid in apex
// and so can only be parsed with explicitly generated code, this option
// was auto selected for you.

public class GoogleCalResponseBodyJSON2Apex {

	public class Reminders {
		public Boolean useDefault {get;set;} 

		public Reminders(JSONParser parser) {
			while (parser.nextToken() != System.JSONToken.END_OBJECT) {
				if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
					String text = parser.getText();
					if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
						if (text == 'useDefault') {
							useDefault = parser.getBooleanValue();
						} else {
							System.debug(LoggingLevel.WARN, 'Reminders consuming unrecognized property: '+text);
							consumeObject(parser);
						}
					}
				}
			}
		}
	}
	
	public class Start {
		public String dateTimeString {get;set;} 

		public Start(JSONParser parser) {
			while (parser.nextToken() != System.JSONToken.END_OBJECT) {
				if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
					String text = parser.getText();
					if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
						if (text == 'dateTime') {
							dateTimeString = parser.getText();
						} else {
							System.debug(LoggingLevel.WARN, 'Start consuming unrecognized property: '+text);
							consumeObject(parser);
						}
					}
				}
			}
		}
	}
	
	public class Creator {
		public String email {get;set;} 
		public String displayName {get;set;} 
		public Boolean self {get;set;} 

		public Creator(JSONParser parser) {
			while (parser.nextToken() != System.JSONToken.END_OBJECT) {
				if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
					String text = parser.getText();
					if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
						if (text == 'email') {
							email = parser.getText();
						} else if (text == 'displayName') {
							displayName = parser.getText();
						} else if (text == 'self') {
							self = parser.getBooleanValue();
						} else {
							System.debug(LoggingLevel.WARN, 'Creator consuming unrecognized property: '+text);
							consumeObject(parser);
						}
					}
				}
			}
		}
	}
	
	public String kind {get;set;} 
	public String etag {get;set;} 
	public String id {get;set;} 
	public String status {get;set;} 
	public String htmlLink {get;set;} 
	public String created {get;set;} 
	public String updated {get;set;} 
	public String summary {get;set;} 
	public String description {get;set;} 
	public Creator creator {get;set;} 
	public Creator organizer {get;set;} 
	public Start start {get;set;} 
	public Start end_Z {get;set;} // in json: end
	public String iCalUID {get;set;} 
	public Integer sequence {get;set;} 
	public Reminders reminders {get;set;} 

	public GoogleCalResponseBodyJSON2Apex(JSONParser parser) {
		while (parser.nextToken() != System.JSONToken.END_OBJECT) {
			if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME) {
				String text = parser.getText();
				if (parser.nextToken() != System.JSONToken.VALUE_NULL) {
					if (text == 'kind') {
						kind = parser.getText();
					} else if (text == 'etag') {
						etag = parser.getText();
					} else if (text == 'id') {
						id = parser.getText();
					} else if (text == 'status') {
						status = parser.getText();
					} else if (text == 'htmlLink') {
						htmlLink = parser.getText();
					} else if (text == 'created') {
						created = parser.getText();
					} else if (text == 'updated') {
						updated = parser.getText();
					} else if (text == 'summary') {
						summary = parser.getText();
					} else if (text == 'description') {
						description = parser.getText();
					} else if (text == 'creator') {
						creator = new Creator(parser);
					} else if (text == 'organizer') {
						organizer = new Creator(parser);
					} else if (text == 'start') {
						start = new Start(parser);
					} else if (text == 'end') {
						end_Z = new Start(parser);
					} else if (text == 'iCalUID') {
						iCalUID = parser.getText();
					} else if (text == 'sequence') {
						sequence = parser.getIntegerValue();
					} else if (text == 'reminders') {
						reminders = new Reminders(parser);
					} else {
						System.debug(LoggingLevel.WARN, 'GoogleCalResponseBodyJSON2Apex consuming unrecognized property: '+text);
						consumeObject(parser);
					}
				}
			}
		}
	}
	
	
	public static GoogleCalResponseBodyJSON2Apex parse(String json) {
		System.JSONParser parser = System.JSON.createParser(json);
		return new GoogleCalResponseBodyJSON2Apex(parser);
	}
	
	public static void consumeObject(System.JSONParser parser) {
		Integer depth = 0;
		do {
			System.JSONToken curr = parser.getCurrentToken();
			if (curr == System.JSONToken.START_OBJECT || 
				curr == System.JSONToken.START_ARRAY) {
				depth++;
			} else if (curr == System.JSONToken.END_OBJECT ||
				curr == System.JSONToken.END_ARRAY) {
				depth--;
			}
		} while (depth > 0 && parser.nextToken() != null);
	}
	

}
```

パーサーのテスト

```GoogleCalResponseBodyJSON2Apex_Test
//
// Generated by JSON2Apex http://json2apex.herokuapp.com/
//

@IsTest
public class GoogleCalResponseBodyJSON2Apex_Test {
	
	// This test method should give 100% coverage
	static testMethod void testParse() {
		String json = '{'+
		'\"kind\": \"calendar#event\",'+
		'\"etag\": \"\\\"3171797152246000\\\"\",'+
		'\"id\": \"fu55dsgo2aqhpc90u7nmabjans\",'+
		'\"status\": \"confirmed\",'+
		'\"htmlLink\": \"https://www.google.com/calendar/event?eid=ZnU1NWRzZ28yYXFocGM5MHU3bm1hYmphbnMgeUBsbmUuc3Q\",'+
		'\"created\": \"2020-04-03T07:22:56.000Z\",'+
		'\"updated\": \"2020-04-03T07:22:56.123Z\",'+
		'\"summary\": \"勤怠日時\",'+
		'\"description\": \"Teamspiritに登録された情報です\",'+
		'\"creator\": {'+
		' \"email\": \"y@lne.st\",'+
		' \"displayName\": \"George Yoshida\",'+
		' \"self\": true'+
		'  },'+
		'  \"organizer\": {'+
		'  	\"email\": \"y@lne.st\",'+
		'  	\"displayName\": \"George Yoshida\",'+
		'  	\"self\": true'+
		'  },'+
		'  \"start\": {'+
		'  	\"dateTime\": \"2020-04-02T09:15:00+09:00\"'+
		'  },'+
		'  \"end\": {'+
		'  	\"dateTime\": \"2020-04-02T09:15:00+09:00\"'+
		'  },    	'+
		'  \"iCalUID\": \"fu55dsgo2aqhpc90u7nmabjans@google.com\",'+
		'  \"sequence\": 0,'+
		'  \"reminders\": {'+
		'  	\"useDefault\": true'+
		'  }'+
		'}';
		GoogleCalResponseBodyJSON2Apex r = GoogleCalResponseBodyJSON2Apex.parse(json);
		System.assert(r != null);

		json = '{\"TestAMissingObject\": { \"TestAMissingArray\": [ { \"TestAMissingProperty\": \"Some Value\" } ] } }';
		GoogleCalResponseBodyJSON2Apex.Reminders objReminders = new GoogleCalResponseBodyJSON2Apex.Reminders(System.JSON.createParser(json));
		System.assert(objReminders != null);
		System.assert(objReminders.useDefault == null);

		json = '{\"TestAMissingObject\": { \"TestAMissingArray\": [ { \"TestAMissingProperty\": \"Some Value\" } ] } }';
		GoogleCalResponseBodyJSON2Apex.Start objStart = new GoogleCalResponseBodyJSON2Apex.Start(System.JSON.createParser(json));
		System.assert(objStart != null);
		System.assert(objStart.dateTimeString == null);

		json = '{\"TestAMissingObject\": { \"TestAMissingArray\": [ { \"TestAMissingProperty\": \"Some Value\" } ] } }';
		GoogleCalResponseBodyJSON2Apex.Creator objCreator = new GoogleCalResponseBodyJSON2Apex.Creator(System.JSON.createParser(json));
		System.assert(objCreator != null);
		System.assert(objCreator.email == null);
		System.assert(objCreator.displayName == null);
		System.assert(objCreator.self == null);

		json = '{\"TestAMissingObject\": { \"TestAMissingArray\": [ { \"TestAMissingProperty\": \"Some Value\" } ] } }';
		GoogleCalResponseBodyJSON2Apex objGoogleCalResponseBodyJSON2Apex = new GoogleCalResponseBodyJSON2Apex(System.JSON.createParser(json));
		System.assert(objGoogleCalResponseBodyJSON2Apex != null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.kind == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.etag == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.id == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.status == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.htmlLink == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.created == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.updated == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.summary == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.description == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.creator == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.organizer == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.start == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.end_Z == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.iCalUID == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.sequence == null);
		System.assert(objGoogleCalResponseBodyJSON2Apex.reminders == null);
	}
}
```

最後にトリガとクラスのテストを書きます。

``` teamspirit_AtkEmpDay_GCalTrrigerTest
@isTest(Seealldata = TRUE)
private class teamspirit_AtkEmpDay_GCalTrrigerTest {
    static testMethod void test1() {
		System.debug('01======================開始=========================');
        LIST<teamspirit__AtkEmpDay__c> empDays = new LIST<teamspirit__AtkEmpDay__c>();
        empDays = [SELECT id,teamspirit__StartTime__c,teamspirit__EndTime__c,CreatedByID FROM teamspirit__AtkEmpDay__c ORDER BY LastModifiedDate DESC LIMIT 1];
        system.debug('empDays ' + empDays);
        user you = new user(id = empDays[0].CreatedByID);
        you.IsTsSyncToGoogleCalendar__c = TRUE;
		update you;        
        empDays[0].teamspirit__StartTime__c = 570;
        empDays[0].teamspirit__EndTime__c = 1200;
        Test.setMock(HttpCalloutMock.class, new HTTPRequestMock_GoogleCalendar(false));
        Test.startTest();
        update empDays[0];
        Test.stopTest();
    }
    static testMethod void test2() {
		System.debug('01======================開始=========================');
        LIST<teamspirit__AtkEmpDay__c> empDays = new LIST<teamspirit__AtkEmpDay__c>();
        empDays = [SELECT id,teamspirit__StartTime__c,teamspirit__EndTime__c,CreatedByID FROM teamspirit__AtkEmpDay__c ORDER BY LastModifiedDate DESC LIMIT 1];
        system.debug('empDays ' + empDays);
        user you = new user(id = empDays[0].CreatedByID);
        you.IsTsSyncToGoogleCalendar__c = TRUE;
		update you;
        Test.setMock(HttpCalloutMock.class, new HTTPRequestMock_GoogleCalendar(false));
        Test.startTest();
        update empDays[0];
        Test.stopTest();
    }
    static testMethod void test3() {
		System.debug('01======================開始=========================');
        LIST<teamspirit__AtkEmpDay__c> empDays = new LIST<teamspirit__AtkEmpDay__c>();
        empDays = [SELECT id,teamspirit__StartTime__c,teamspirit__EndTime__c,CreatedByID FROM teamspirit__AtkEmpDay__c ORDER BY LastModifiedDate DESC LIMIT 1];
        system.debug('empDays ' + empDays);
        user you = new user(id = empDays[0].CreatedByID);
        you.IsTsSyncToGoogleCalendar__c = TRUE;
		update you;        
        empDays[0].teamspirit__StartTime__c = 570;
        empDays[0].teamspirit__EndTime__c = 1200;
        Test.setMock(HttpCalloutMock.class, new HTTPRequestMock_GoogleCalendar(false));
        empDays[0].teamspirit__StartTime__c = NULL;
        empDays[0].teamspirit__EndTime__c = NULL;
        empDays[0].Google_Calendar_eventId__c = 'test';
        Test.startTest();
        update empDays[0];
        Test.stopTest();
    }
}
```

TestMethod として定義されたメソッドは、Web サービスコールアウトをサポートしません。
と怒られてしまうので、モッククラスを作って読み込んでいます。

``` HTTPRequestMock_GoogleCalendar
@isTest
global class HTTPRequestMock_GoogleCalendar implements HttpCalloutMock {
    // https://www.terrasky.co.jp/blog/2016/160120_001558.php
    // エラー発生フラグ
    public Boolean errorFlg { get; set; }
    /**
    * @author George
    * @description コンストラクタ
    * @param errFlg エラー発生フラグ
    *
    */
    public HTTPRequestMock_GoogleCalendar(Boolean errFlg) {
    	this.errorFlg = errFlg;
    }
    /**
    * @description テストで返すレスポンスを設定する
    * @param req HTTPリクエストが渡される
    * @return GoogleCalendarレスポンスのテストデータ
    */
    global HTTPResponse respond(HTTPRequest req) {
    // エラーフラグがtrueだった場合は通信エラーを投げる
    if (this.errorFlg) {
    	throw new System.CalloutException('通信エラー');
    }
    // レスポンスデータ
    Map<String, Object> responseMap = new Map<String, Object>();
    HttpResponse res = new HttpResponse();
    res.setHeader('Content-Type', 'application/json');
    // JSONデータとしてBodyにセット
        String json = '{'+
            '\"kind\": \"calendar#event\",'+
            '\"etag\": \"\\\"3171797152246000\\\"\",'+
            '\"id\": \"fu55dsgo2aqhpc90u7nmabjans\",'+
            '\"status\": \"confirmed\",'+
            '\"htmlLink\": \"https://www.google.com/calendar/event?eid=ZnU1NWRzZ28yYXFocGM5MHU3bm1hYmphbnMgeUBsbmUuc3Q\",'+
            '\"created\": \"2020-04-03T07:22:56.000Z\",'+
            '\"updated\": \"2020-04-03T07:22:56.123Z\",'+
            '\"summary\": \"勤怠日時\",'+
            '\"description\": \"Teamspiritに登録された情報です\",'+
            '\"creator\": {'+
            ' \"email\": \"y@lne.st\",'+
            ' \"displayName\": \"George Yoshida\",'+
            ' \"self\": true'+
            '  },'+
            '  \"organizer\": {'+
            '  	\"email\": \"y@lne.st\",'+
            '  	\"displayName\": \"George Yoshida\",'+
            '  	\"self\": true'+
            '  },'+
            '  \"start\": {'+
            '  	\"dateTime\": \"2020-04-02T09:15:00+09:00\"'+
            '  },'+
            '  \"end\": {'+
            '  	\"dateTime\": \"2020-04-02T09:15:00+09:00\"'+
            '  },    	'+
            '  \"iCalUID\": \"fu55dsgo2aqhpc90u7nmabjans@google.com\",'+
            '  \"sequence\": 0,'+
            '  \"reminders\": {'+
            '  	\"useDefault\": true'+
            '  }'+
            '}';

        res.setBody(json);
    res.setStatusCode(200);
    return res;
    }
}

```